From b00eddda7e8d470e7b475b1e261bbb303c90db59 Mon Sep 17 00:00:00 2001 From: Tony Espinoza <86493411+tonyespinoza1@users.noreply.github.com> Date: Mon, 8 Dec 2025 13:58:50 -0800 Subject: [PATCH 1/4] docs: Add reliable process shutdown guide for local dev servers (#2215) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs: Add reliable process shutdown section to LOCAL_DEV_SERVERS.md Add guidance for managing dev server processes when PIDs weren't tracked: - Force-kill by port using lsof - Verification steps before restart - Complete restart workflow script - Troubleshooting stubborn/orphaned processes This addresses issues with orphaned deno processes blocking port reuse. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Tony Espinoza Co-authored-by: Claude Opus 4.5 --- docs/common/LOCAL_DEV_SERVERS.md | 80 ++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/docs/common/LOCAL_DEV_SERVERS.md b/docs/common/LOCAL_DEV_SERVERS.md index 69fb82f61a..6a861e3e50 100644 --- a/docs/common/LOCAL_DEV_SERVERS.md +++ b/docs/common/LOCAL_DEV_SERVERS.md @@ -106,6 +106,86 @@ curl http://localhost:8000/_health && curl http://localhost:5173 kill $TOOLSHED_PID $SHELL_PID ``` +## Reliable Process Shutdown + +When PIDs weren't tracked or processes become orphaned, use port-based termination: + +### Force-Kill by Port + +```bash +# Kill any process on port 8000 (Toolshed) +lsof -ti :8000 | xargs kill -9 2>/dev/null + +# Kill any process on port 5173 (Shell) +lsof -ti :5173 | xargs kill -9 2>/dev/null +``` + +### Verify Ports Are Free + +Always verify before restarting: + +```bash +# Should return nothing if ports are free +lsof -i :8000 +lsof -i :5173 +``` + +### Complete Restart Workflow + +```bash +# 1. Stop all processes on both ports +lsof -ti :8000 | xargs kill -9 2>/dev/null +lsof -ti :5173 | xargs kill -9 2>/dev/null + +# 2. Wait briefly for cleanup +sleep 2 + +# 3. Verify ports are free +if lsof -i :8000 || lsof -i :5173; then + echo "ERROR: Ports still in use" + exit 1 +fi + +# 4. Start servers (in separate terminals or background) +cd packages/toolshed && SHELL_URL=http://localhost:5173 deno task dev & +TOOLSHED_PID=$! + +cd packages/shell && deno task dev-local & +SHELL_PID=$! + +# 5. Wait for startup +sleep 5 + +# 6. Verify both are healthy +curl -sf http://localhost:8000/_health > /dev/null && echo "Backend: OK" || echo "Backend: FAILED" +curl -sf http://localhost:5173 > /dev/null && echo "Frontend: OK" || echo "Frontend: FAILED" +``` + +### Troubleshooting Stubborn Processes + +If `lsof -ti :PORT | xargs kill -9` doesn't work: + +1. **Check for multiple processes:** + ```bash + lsof -i :8000 # Lists all processes with details + ``` + +2. **Kill by process name (nuclear option):** + ```bash + pkill -9 -f "deno task dev" + ``` + +3. **Check for child processes:** + ```bash + pgrep -f deno | xargs ps -p + ``` + +4. **Last resort - kill all deno processes:** + ```bash + pkill -9 -f deno + ``` + ⚠️ This kills ALL deno processes, not just dev servers. + ## Integration with Pattern Development When deploying patterns locally: From 72686e5382ffc2a397268399d3893698115fc478 Mon Sep 17 00:00:00 2001 From: Ben Follington <5009316+bfollington@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:59:58 +1000 Subject: [PATCH 2/4] Update `todo-list.tsx` with `ct-textarea` and `wish()` (#2222) --- packages/html/src/jsx.d.ts | 46 +++++++++++++++++++++++++++++++++ packages/patterns/todo-list.tsx | 42 ++++++++++-------------------- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/packages/html/src/jsx.d.ts b/packages/html/src/jsx.d.ts index 65fe7c9bbe..4014464ceb 100644 --- a/packages/html/src/jsx.d.ts +++ b/packages/html/src/jsx.d.ts @@ -2864,6 +2864,7 @@ interface CTListElement extends CTHTMLElement {} interface CTListItemElement extends CTHTMLElement {} interface CTLoaderElement extends CTHTMLElement {} interface CTInputElement extends CTHTMLElement {} +interface CTTextAreaElement extends CTHTMLElement {} interface CTFileInputElement extends CTHTMLElement {} interface CTImageInputElement extends CTHTMLElement {} interface CTInputLegacyElement extends CTHTMLElement {} @@ -3261,6 +3262,47 @@ interface CTInputAttributes extends CTHTMLAttributes { "onct-invalid"?: any; } +interface CTTextAreaAttributes extends CTHTMLAttributes { + "$value"?: CellLike; + "value"?: CellLike | string; + "placeholder"?: string; + "disabled"?: boolean; + "readonly"?: boolean; + "error"?: boolean; + "name"?: string; + "required"?: boolean; + "autofocus"?: boolean; + "rows"?: number; + "cols"?: number; + "maxlength"?: string; + "minlength"?: string; + "wrap"?: string; + "spellcheck"?: boolean; + "autocomplete"?: string; + "resize"?: string; + "auto-resize"?: boolean; + "timing-strategy"?: "immediate" | "debounce" | "throttle" | "blur"; + "timing-delay"?: number; + "onct-input"?: EventHandler< + { value: string; oldValue: string; name: string } + >; + "onct-change"?: EventHandler< + { value: string; oldValue: string; name: string } + >; + "onct-focus"?: EventHandler<{ value: string; name: string }>; + "onct-blur"?: EventHandler<{ value: string; name: string }>; + "onct-keydown"?: EventHandler<{ + key: string; + value: string; + shiftKey: boolean; + ctrlKey: boolean; + metaKey: boolean; + altKey: boolean; + name: string; + }>; + "onct-submit"?: EventHandler<{ value: string; name: string }>; +} + interface CTInputLegacyAttributes extends CTHTMLAttributes { "value"?: CellLike; "placeholder"?: string; @@ -3905,6 +3947,10 @@ declare global { CTInputAttributes, CTInputElement >; + "ct-textarea": CTDOM.DetailedHTMLProps< + CTTextAreaAttributes, + CTTextAreaElement + >; "ct-file-input": CTDOM.DetailedHTMLProps< CTFileInputAttributes, CTFileInputElement diff --git a/packages/patterns/todo-list.tsx b/packages/patterns/todo-list.tsx index 7c4364c1c6..6f18d5018e 100644 --- a/packages/patterns/todo-list.tsx +++ b/packages/patterns/todo-list.tsx @@ -1,6 +1,5 @@ /// -import { Cell, Default, derive, NAME, pattern, UI } from "commontools"; -import Suggestion from "./suggestion.tsx"; +import { Cell, Default, NAME, pattern, UI, wish } from "commontools"; interface TodoItem { title: string; @@ -16,13 +15,6 @@ interface Output { } export default pattern(({ items }) => { - // AI suggestion based on current todos - const suggestion = Suggestion({ - situation: - "Based on my todo list, use a pattern to help me. For sub-tasks and additional tasks, use a todo list.", - context: { items }, - }); - return { [NAME]: "Todo with Suggestions", [UI]: ( @@ -43,6 +35,12 @@ export default pattern(({ items }) => { {/* Todo items with per-item suggestions */}
{items.map((item) => { + // AI suggestion based on current todos + const wishResult = wish({ + query: item.title, + context: { item, items }, + }); + return (
(({ items }) => { ? { textDecoration: "line-through", opacity: 0.6 } : {}} > - {item.title} + (({ items }) => { ×
+ +
+ AI Suggestion + {wishResult} +
); })} - - {/* AI Suggestion */} - -
-

AI Suggestion

- {derive(suggestion, (s) => - s?.result ?? ( - Getting suggestion... - ))} -
-
), items, - suggestion, }; }); From 2256b756f036aa0870ad55a2524cb4bbf6bc97ec Mon Sep 17 00:00:00 2001 From: gideon Date: Tue, 9 Dec 2025 06:20:30 +0800 Subject: [PATCH 3/4] Attempted fix for solitaire drag-and-drop (#2227) DOES NOT actually fix solitaire drag-and-drop bug, but is nevertheless an improvement to correctness. // Bug: .delete loses 'this' binding when passed directly to forEach .forEach(this.resultRecipeCache.delete); // Fix: Wrap in arrow function to preserve binding .forEach((key) => this.resultRecipeCache.delete(key)); --- packages/runner/src/runner.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/runner/src/runner.ts b/packages/runner/src/runner.ts index 31ab842b8b..4606692e57 100644 --- a/packages/runner/src/runner.ts +++ b/packages/runner/src/runner.ts @@ -95,7 +95,7 @@ export class Runner implements IRunner { // copy keys, since we'll mutate the collection while iterating const cacheKeys = [...this.resultRecipeCache.keys()]; cacheKeys.filter((key) => key.startsWith(`${notification.space}/`)) - .forEach(this.resultRecipeCache.delete); + .forEach((key) => this.resultRecipeCache.delete(key)); } return { done: false }; }, From 3b0b172d2e827b931c51cf1ee623c0b304e81ac9 Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Mon, 8 Dec 2025 16:23:46 -0800 Subject: [PATCH 4/4] feat: Support space DIDs in URLs (#2230) --- packages/cli/commands/acl.ts | 2 +- packages/cli/lib/acl.ts | 18 +++-- packages/cli/lib/charm.ts | 11 +-- packages/shell/src/lib/app/view.ts | 13 ++-- packages/shell/src/lib/runtime.ts | 113 +++++++++++++++-------------- 5 files changed, 85 insertions(+), 72 deletions(-) diff --git a/packages/cli/commands/acl.ts b/packages/cli/commands/acl.ts index 42a25e1655..5331df26f9 100644 --- a/packages/cli/commands/acl.ts +++ b/packages/cli/commands/acl.ts @@ -121,6 +121,6 @@ function parseSpaceOptions( return { apiUrl: new URL(apiUrl), identityPath: identity, - spaceName: space, + space: space, }; } diff --git a/packages/cli/lib/acl.ts b/packages/cli/lib/acl.ts index 48ee8a280f..acefee642b 100644 --- a/packages/cli/lib/acl.ts +++ b/packages/cli/lib/acl.ts @@ -1,4 +1,4 @@ -import { createSession, Session } from "@commontools/identity"; +import { createSession, isDID, Session } from "@commontools/identity"; import { loadIdentity } from "./identity.ts"; import { Runtime } from "@commontools/runner"; import { StorageManager } from "@commontools/runner/storage/cache"; @@ -13,17 +13,21 @@ import { ACLManager } from "@commontools/charm/ops"; export interface SpaceConfig { apiUrl: URL; identityPath: string; - spaceName: string; + space: string; } // Create an identity and session from configuration. async function loadSession(config: SpaceConfig): Promise { const identity = await loadIdentity(config.identityPath); - const session = await createSession({ - identity, - spaceName: config.spaceName, - }); - return session; + return isDID(config.space) + ? createSession({ + identity, + spaceDid: config.space, + }) + : createSession({ + identity, + spaceName: config.space, + }); } // Creates a Runtime instance for ACL operations diff --git a/packages/cli/lib/charm.ts b/packages/cli/lib/charm.ts index 6645bbf786..a0fae56475 100644 --- a/packages/cli/lib/charm.ts +++ b/packages/cli/lib/charm.ts @@ -1,4 +1,4 @@ -import { createSession, Session } from "@commontools/identity"; +import { createSession, isDID, Session } from "@commontools/identity"; import { ensureDir } from "@std/fs"; import { loadIdentity } from "./identity.ts"; import { @@ -32,11 +32,12 @@ export interface CharmConfig extends SpaceConfig { } async function makeSession(config: SpaceConfig): Promise { - if (config.space.startsWith("did:key")) { - throw new Error("DID key spaces not yet supported."); - } const identity = await loadIdentity(config.identity); - return createSession({ identity, spaceName: config.space }); + if (isDID(config.space)) { + return createSession({ identity, spaceDid: config.space }); + } else { + return createSession({ identity, spaceName: config.space }); + } } export async function loadManager(config: SpaceConfig): Promise { diff --git a/packages/shell/src/lib/app/view.ts b/packages/shell/src/lib/app/view.ts index a4f05df4ba..7a5836647e 100644 --- a/packages/shell/src/lib/app/view.ts +++ b/packages/shell/src/lib/app/view.ts @@ -46,7 +46,6 @@ export function appViewToUrlPath(view: AppView): `/${string}` { ? `/${view.spaceName}/${view.charmId}` : `/${view.spaceName}`; } else if ("spaceDid" in view) { - // did routes not yet supported return "charmId" in view ? `/${view.spaceDid}/${view.charmId}` : `/${view.spaceDid}`; @@ -59,10 +58,12 @@ export function urlToAppView(url: URL): AppView { segments.shift(); // shift off the pathnames' prefix "/"; const [first, charmId] = [segments[0], segments[1]]; - if (charmId) { - return { spaceName: first, charmId }; - } else if (first) { - return { spaceName: first }; + if (!first) { + return { builtin: "home" }; + } + if (isDID(first)) { + return charmId ? { spaceDid: first, charmId } : { spaceDid: first }; + } else { + return charmId ? { spaceName: first, charmId } : { spaceName: first }; } - return { builtin: "home" }; } diff --git a/packages/shell/src/lib/runtime.ts b/packages/shell/src/lib/runtime.ts index 4cba3401e1..a6f317f735 100644 --- a/packages/shell/src/lib/runtime.ts +++ b/packages/shell/src/lib/runtime.ts @@ -1,4 +1,4 @@ -import { createSession, Identity } from "@commontools/identity"; +import { createSession, DID, Identity } from "@commontools/identity"; import { Runtime, RuntimeTelemetry, @@ -40,20 +40,20 @@ export class RuntimeInternals extends EventTarget { #inspector: Inspector.Channel; #disposed = false; #space: string; // The MemorySpace DID - #spaceRootPattern?: CharmController; - #spaceRootPatternType: PatternFactory.BuiltinPatternType; - #patternCache: Map> = new Map(); + #spaceRootPatternId?: string; + #isHomeSpace: boolean; + #patternCache: PatternCache; private constructor( cc: CharmsController, telemetry: RuntimeTelemetry, space: string, - spaceRootPatternType: PatternFactory.BuiltinPatternType, + isHomeSpace: boolean, ) { super(); this.#cc = cc; this.#space = space; - this.#spaceRootPatternType = spaceRootPatternType; + this.#isHomeSpace = isHomeSpace; const runtimeId = this.#cc.manager().runtime.id; this.#inspector = new Inspector.Channel( runtimeId, @@ -62,6 +62,7 @@ export class RuntimeInternals extends EventTarget { this.#telemetry = telemetry; this.#telemetry.addEventListener("telemetry", this.#onTelemetry); this.#telemetryMarkers = []; + this.#patternCache = new PatternCache(this.#cc); } telemetry(): RuntimeTelemetryMarkerResult[] { @@ -85,35 +86,27 @@ export class RuntimeInternals extends EventTarget { // based on the view type (home vs space). async getSpaceRootPattern(): Promise> { this.#check(); - if (this.#spaceRootPattern) { - return this.#spaceRootPattern; + if (this.#spaceRootPatternId) { + return this.getPattern(this.#spaceRootPatternId); } const pattern = await PatternFactory.getOrCreate( this.#cc, - this.#spaceRootPatternType, + this.#isHomeSpace ? "home" : "space-root", ); - this.#spaceRootPattern = pattern; - this.#patternCache.set(pattern.id, pattern); - // Track as recently accessed - await this.#cc.manager().trackRecentCharm(pattern.getCell()); + this.#spaceRootPatternId = pattern.id; + await this.#patternCache.add(pattern); return pattern; } - // Returns a pattern by ID, starting it if requested. - // Patterns are cached by ID. async getPattern(id: string): Promise> { this.#check(); - const cached = this.#patternCache.get(id); + const cached = await this.#patternCache.get(id); if (cached) { return cached; } const pattern = await this.#cc.get(id, true, nameSchema); - - // Track as recently accessed - await this.#cc.manager().trackRecentCharm(pattern.getCell()); - - this.#patternCache.set(id, pattern); + await this.#patternCache.add(pattern); return pattern; } @@ -154,24 +147,22 @@ export class RuntimeInternals extends EventTarget { ): Promise { let session; let spaceName; - let spaceRootPatternType: PatternFactory.BuiltinPatternType; + let isHomeSpace = false; if ("builtin" in view) { switch (view.builtin) { case "home": session = await createSession({ identity, spaceDid: identity.did() }); spaceName = ""; - spaceRootPatternType = "home"; + isHomeSpace = true; break; } } else if ("spaceName" in view) { session = await createSession({ identity, spaceName: view.spaceName }); spaceName = view.spaceName; - spaceRootPatternType = "space-root"; } else if ("spaceDid" in view) { session = await createSession({ identity, spaceDid: view.spaceDid }); - spaceRootPatternType = "space-root"; } - if (!session || !spaceRootPatternType!) { + if (!session) { throw new Error("Unexpected view provided."); } @@ -214,15 +205,10 @@ export class RuntimeInternals extends EventTarget { if (!id) { throw new Error(`Could not navigate to cell that is not a charm.`); } - - // NOTE(jake): Eventually, once we're doing multi-space navigation, we - // will need to replace this charmManager.getSpaceName() with a call to - // some sort of address book / dns-style server, OR just navigate to the - // DID. - - // Get the space name for navigation until we support - // DID spaces from the shell. - const spaceName = charmManager.getSpaceName(); + const navigateCallback = createNavCallback( + session.space, + charmManager.getSpaceName(), + ); // Await storage being synced, at least for now, as the page fully // reloads. Once we have in-page navigation with reloading, we don't @@ -246,25 +232,10 @@ export class RuntimeInternals extends EventTarget { await charmManager.add([target]); } - if (!spaceName) { - throw new Error( - "Does not yet support navigating to a charm within a space loaded by DID.", - ); - } - // Use the human-readable space name from CharmManager instead of DID - navigate({ - spaceName, - charmId: id, - }); + navigateCallback(id); }).catch((err) => { console.error("[navigateCallback] Error during storage sync:", err); - - if (spaceName) { - navigate({ - spaceName, - charmId: id, - }); - } + navigateCallback(id); }); }, }); @@ -290,7 +261,43 @@ export class RuntimeInternals extends EventTarget { cc, telemetry, session.space, - spaceRootPatternType, + isHomeSpace, ); } } + +// Caches patterns, and updates recent charms data upon access. +class PatternCache { + private cache: Map> = new Map(); + private cc: CharmsController; + + constructor(cc: CharmsController) { + this.cc = cc; + } + + async add(pattern: CharmController) { + this.cache.set(pattern.id, pattern); + await this.cc.manager().trackRecentCharm(pattern.getCell()); + } + + async get(id: string): Promise | undefined> { + const cached = this.cache.get(id); + if (cached) { + await this.cc.manager().trackRecentCharm(cached.getCell()); + return cached; + } + } +} + +function createNavCallback(spaceDid: DID, spaceName?: string) { + return (id: string) => { + if (spaceName) { + navigate({ + spaceName, + charmId: id, + }); + } else { + navigate({ spaceDid, charmId: id }); + } + }; +}