@cortexkit/aft-opencode 0.45.1 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,13 +7,15 @@
7
7
  * it shows in the right place in the OpenCode UI, and carries `noReply: true`
8
8
  * so no LLM call is made.
9
9
  *
10
- * IMPORTANT (cache + crash safety): this path deliberately passes ONLY `agent`
11
- * (never model/variant). OpenCode crashes if model/variant are supplied on a
12
- * `noReply: true` prompt, and omitting them keeps the synthetic message from
13
- * busting the provider prefix cache the previous assistant turn warmed.
14
- *
15
- * Lives in `shared/` (not `index.ts`) because `index.ts` must export only the
16
- * plugin default; both the plugin entry and `tools/permissions.ts` call this.
10
+ * Model/variant ARE passed when resolvable, mirroring the session's newest
11
+ * context: OpenCode's `createUserMessage` resolves an omitted model as
12
+ * `agent.model ?? session-current ?? default` and PERSISTS the result via
13
+ * `setAgentModel` — so omitting the model on a session whose user picked a
14
+ * non-default model silently resets the session's model/variant (observed on
15
+ * OpenCode Desktop via /aft-status). Passing the resolved current context
16
+ * makes that persistence a no-op. Older OpenCode builds crashed when model
17
+ * was supplied on a `noReply: true` prompt, so on any failure we retry once
18
+ * without model/variant rather than dropping the message.
17
19
  */
18
20
  export declare function sendIgnoredMessage(client: unknown, sessionID: string, text: string): Promise<void>;
19
21
  //# sourceMappingURL=ignored-message.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ignored-message.d.ts","sourceRoot":"","sources":["../../src/shared/ignored-message.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAqCf"}
1
+ {"version":3,"file":"ignored-message.d.ts","sourceRoot":"","sources":["../../src/shared/ignored-message.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,OAAO,EACf,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAsDf"}
@@ -15,16 +15,34 @@ export interface AftRpcCallOptions {
15
15
  * placeholder wins.
16
16
  */
17
17
  accept?: (result: unknown) => boolean;
18
+ /**
19
+ * Internal one-hop guard for verified-directory redirects. A redirected
20
+ * call must never redirect again (placeholder chains would otherwise
21
+ * ping-pong between two bridgeless instances).
22
+ */
23
+ noRedirect?: boolean;
18
24
  }
25
+ export declare function subscribeRpcRedirects(listener: (from: string, to: string) => void): () => void;
26
+ export declare function __resetRpcRedirectsForTest(): void;
27
+ export declare function __recordRpcRedirectForTest(from: string, to: string): void;
19
28
  export declare class AftRpcClient {
20
29
  private port;
21
30
  private token;
22
31
  private portsDir;
23
32
  private legacyPortFile;
24
33
  private stalePortFailures;
34
+ private storageDir;
35
+ private directory;
25
36
  constructor(storageDir: string, directory: string);
26
37
  /** Call an RPC method. Retries port resolution if the server isn't ready yet. */
27
38
  call<T = Record<string, unknown>>(method: string, params?: Record<string, unknown>, options?: AftRpcCallOptions): Promise<T>;
39
+ /**
40
+ * One-hop redirect to the instance that owns the session's bridge. Returns
41
+ * null (caller keeps the placeholder) unless the redirected call produced a
42
+ * warm response — a placeholder from the target is not an improvement, and
43
+ * failures must not mask the original placeholder.
44
+ */
45
+ private followVerifiedDirectory;
28
46
  private callOne;
29
47
  /**
30
48
  * Heuristic for "this response is the lazy-spawn placeholder, not the real
@@ -34,7 +52,11 @@ export declare class AftRpcClient {
34
52
  private looksLikePlaceholder;
35
53
  /** Check if any RPC server is reachable. */
36
54
  isAvailable(): Promise<boolean>;
37
- /** Resolve the freshest live endpoint for the persistent TUI WebSocket. */
55
+ /** Resolve the freshest live endpoint for the persistent TUI WebSocket.
56
+ * Follows a learned verified-directory redirect so the socket subscribes
57
+ * to the instance that owns the session's bridge (the one that will
58
+ * actually push status-changed frames), not this directory's bridgeless
59
+ * placeholder instance. */
38
60
  resolveEndpoint(signal?: AbortSignal): Promise<AftRpcEndpoint | null>;
39
61
  /**
40
62
  * Discover all live RPC port files for this project. Tries the per-instance
@@ -1 +1 @@
1
- {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;CACvC;AA4BD,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAA6B;gBAE1C,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAKjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,CAAC,CAAC;YA8CC,OAAO;IAsBrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAM5B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC,2EAA2E;IACrE,eAAe,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAM3E;;;;OAIG;YACW,gBAAgB;IAwB9B,OAAO,CAAC,gBAAgB;IAwDxB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,iBAAiB;YA6BX,WAAW;YAkCX,gBAAgB;IAmB9B,KAAK,IAAI,IAAI;CAKd"}
1
+ {"version":3,"file":"rpc-client.d.ts","sourceRoot":"","sources":["../../src/shared/rpc-client.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;;;;;;;;OASG;IACH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,KAAK,OAAO,CAAC;IACtC;;;;OAIG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAuBD,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,KAAK,IAAI,GAAG,MAAM,IAAI,CAK9F;AAcD,wBAAgB,0BAA0B,IAAI,IAAI,CAGjD;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,IAAI,CAEzE;AA4BD,qBAAa,YAAY;IACvB,OAAO,CAAC,IAAI,CAAuB;IACnC,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,QAAQ,CAAS;IACzB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,iBAAiB,CAA6B;IACtD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAS;gBAEd,UAAU,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;IAOjD,iFAAiF;IAC3E,IAAI,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EACpC,MAAM,EAAE,MAAM,EACd,MAAM,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,EACpC,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,CAAC,CAAC;IA0Db;;;;;OAKG;YACW,uBAAuB;YAwBvB,OAAO;IAsBrB;;;;OAIG;IACH,OAAO,CAAC,oBAAoB;IAM5B,4CAA4C;IACtC,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC;;;;gCAI4B;IACtB,eAAe,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;IAa3E;;;;OAIG;YACW,gBAAgB;IAwB9B,OAAO,CAAC,gBAAgB;IAwDxB,8EAA8E;IAC9E,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,aAAa;IASrB,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,iBAAiB;YA6BX,WAAW;YAkCX,gBAAgB;IAmB9B,KAAK,IAAI,IAAI;CAKd"}
@@ -1 +1 @@
1
- {"version":3,"file":"subc-tool-schemas.d.ts","sourceRoot":"","sources":["../src/subc-tool-schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAUhD,QAAA,MAAM,eAAe,sOAsBX,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,wBAAgB,qBAAqB,IAAI,aAAa,CAYrD;AAQD;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAqFxF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAajG;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,eAAO,MAAM,oBAAoB,EAAE,SAAS,gBAAgB,EAAoB,CAAC"}
1
+ {"version":3,"file":"subc-tool-schemas.d.ts","sourceRoot":"","sources":["../src/subc-tool-schemas.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAkBH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAYhD,QAAA,MAAM,eAAe,sOAsBX,CAAC;AAEX,MAAM,MAAM,gBAAgB,GAAG,CAAC,OAAO,eAAe,CAAC,CAAC,MAAM,CAAC,CAAC;AAEhE,wBAAgB,qBAAqB,IAAI,aAAa,CAYrD;AAWD;;GAEG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAAC,gBAAgB,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAqFxF;AAED;;GAEG;AACH,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,MAAM,CAajG;AAED,wBAAgB,wBAAwB,IAAI,MAAM,CAEjD;AAED,eAAO,MAAM,oBAAoB,EAAE,SAAS,gBAAgB,EAAoB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAsCjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AAwQtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAuHjE;AAwkBD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0B/E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAqGnF"}
1
+ {"version":3,"file":"hoisted.d.ts","sourceRoot":"","sources":["../../src/tools/hoisted.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAKH,OAAO,KAAK,EAAE,cAAc,EAAc,MAAM,qBAAqB,CAAC;AAGtE,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAsCjD,wEAAwE;AACxE,eAAO,MAAM,wBAAwB,GAAI,IAAI,MAAM,EAAE,QAAQ,MAAM,EAAE,OAAO,MAAM,KAAG,MAChD,CAAC;AAwQtC;;GAEG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,aAAa,GAAG,cAAc,CAuHjE;AA8mBD;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA0B/E;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAqGnF"}
@@ -65,6 +65,13 @@ export declare const _permissionsInternalsForTest: {
65
65
  export declare function assertExternalDirectoryPermission(ctx: PluginContext, context: ToolContext, target: string, options?: {
66
66
  kind?: "file" | "directory";
67
67
  }): Promise<string | undefined>;
68
+ /**
69
+ * Ask OpenCode for permission to expose indexed content from a different git root.
70
+ * This deliberately uses a tool-specific permission id rather than
71
+ * `external_directory`: a user may allow ordinary path reads while still wanting
72
+ * a separate audit point for search results produced from borrowed indexes.
73
+ */
74
+ export declare function assertAftSearchExternalPermission(ctx: PluginContext, context: ToolContext, target: string): Promise<string | undefined>;
68
75
  /**
69
76
  * Trigger the host OpenCode permission check for grep.
70
77
  *
@@ -1 +1 @@
1
- {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/tools/permissions.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAsDjD;;;;;;;;;;;GAWG;AACH,wBAAsB,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,MAAM,GACnB,MAAM,CAER;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAazF;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAE9D;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,MAAM,EAAE,EAClB,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACrC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkB7B;AAED;;;GAGG;AACH,iBAAS,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAI5D;AASD,iBAAS,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjD;AA6DD;;;;;;;;;;;;GAYG;AACH,iBAAS,oBAAoB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED,eAAO,MAAM,4BAA4B;;;;CAIxC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iCAAiC,CACrD,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAqE7B;AAkCD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkB7B;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOhE"}
1
+ {"version":3,"file":"permissions.d.ts","sourceRoot":"","sources":["../../src/tools/permissions.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAGvD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAwDjD;;;;;;;;;;;GAWG;AACH,wBAAsB,MAAM,CAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAEhE;AAED,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAGhF;AAED,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAEnF;AAED,wBAAgB,kCAAkC,CAChD,OAAO,EAAE,WAAW,EACpB,YAAY,EAAE,MAAM,GACnB,MAAM,CAER;AAED,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAazF;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,WAAW,GAAG,MAAM,CAE9D;AAED,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,QAAQ,EAAE,MAAM,EAAE,EAClB,QAAQ,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACrC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkB7B;AAED;;;GAGG;AACH,iBAAS,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAI5D;AASD,iBAAS,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAGjD;AA6DD;;;;;;;;;;;;GAYG;AACH,iBAAS,oBAAoB,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAO/C;AAED,eAAO,MAAM,4BAA4B;;;;CAIxC,CAAC;AAEF;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAsB,iCAAiC,CACrD,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,EACd,OAAO,CAAC,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,GAAG,WAAW,CAAA;CAAE,GACxC,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAqE7B;AAwBD;;;;;GAKG;AACH,wBAAsB,iCAAiC,CACrD,GAAG,EAAE,aAAa,EAClB,OAAO,EAAE,WAAW,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAsE7B;AAkCD;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED;;;;;;GAMG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAO,GACjD,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAE7B;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,WAAW,EACpB,OAAO,EAAE,MAAM,EACf,QAAQ,GAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAO,GAC/B,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkB7B;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAOhE"}
@@ -1 +1 @@
1
- {"version":3,"file":"semantic.d.ts","sourceRoot":"","sources":["../../src/tools/semantic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAYjD,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA+EhF"}
1
+ {"version":3,"file":"semantic.d.ts","sourceRoot":"","sources":["../../src/tools/semantic.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAE1D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAgBjD,wBAAgB,aAAa,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CA+FhF"}
@@ -1 +1 @@
1
- {"version":3,"file":"notification-socket.d.ts","sourceRoot":"","sources":["../../src/tui/notification-socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,sBAAsB,CAAC;AAGzE,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAClC,cAAc,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClF;AAED,UAAU,aAAa;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CACd,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,EAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,KAAK,gBAAgB,GAAG,KAAK,GAAG,EAAE,MAAM,KAAK,aAAa,CAAC;AAC3D,KAAK,aAAa,GAAG,IAAI,CAAC,YAAY,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC;AAErE,UAAU,UAAU;IAClB,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,aAAa,CAAC;IACnD,aAAa,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACvC,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,YAAY,EAAE,OAAO,YAAY,CAAC;CACnC;AA8CD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAKjE;AAED,wBAAgB,gBAAgB,IAAI,IAAI,CAavC;AAED,wBAAgB,wBAAwB,IAAI,IAAI,CAG/C;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAC5C,MAAM,IAAI,CAKZ;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACnC,OAAO,EAAE,MAAM,GACd;IAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAoB/C;AA4PD,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,MAAM,IAAI,CAMvF;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAiBjD"}
1
+ {"version":3,"file":"notification-socket.d.ts","sourceRoot":"","sources":["../../src/tui/notification-socket.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAA8C,MAAM,sBAAsB,CAAC;AAGhG,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C,YAAY,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAClC,cAAc,EAAE,CAAC,YAAY,EAAE,kBAAkB,KAAK,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAClF;AAED,UAAU,aAAa;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,CACd,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,EAC5C,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GACjC,IAAI,CAAC;IACR,IAAI,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,IAAI,IAAI,CAAC;CACf;AAED,KAAK,gBAAgB,GAAG,KAAK,GAAG,EAAE,MAAM,KAAK,aAAa,CAAC;AAC3D,KAAK,aAAa,GAAG,IAAI,CAAC,YAAY,EAAE,iBAAiB,GAAG,OAAO,CAAC,CAAC;AAErE,UAAU,UAAU;IAClB,YAAY,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,aAAa,CAAC;IACnD,aAAa,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACvC,UAAU,EAAE,OAAO,UAAU,CAAC;IAC9B,YAAY,EAAE,OAAO,YAAY,CAAC;CACnC;AA8CD,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CAMjE;AAwBD,wBAAgB,gBAAgB,IAAI,IAAI,CAiBvC;AAED,wBAAgB,wBAAwB,IAAI,IAAI,CAG/C;AAED,wBAAgB,4BAA4B,CAC1C,QAAQ,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,IAAI,GAC5C,MAAM,IAAI,CAKZ;AAED,wBAAgB,4BAA4B,CAC1C,OAAO,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACnC,OAAO,EAAE,MAAM,GACd;IAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAoB/C;AA4PD,wBAAgB,4BAA4B,CAAC,SAAS,EAAE,OAAO,CAAC,UAAU,CAAC,GAAG,MAAM,IAAI,CAMvF;AAED,wBAAgB,0BAA0B,IAAI,IAAI,CAiBjD"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.45.1",
3
+ "version": "0.46.0",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Agent File Tools (AFT) — tree-sitter and lsp powered code analysis",
6
6
  "main": "dist/index.js",
@@ -13,13 +13,14 @@
13
13
  "files": [
14
14
  "dist",
15
15
  "src/tui",
16
+ "src/tui-compiled",
16
17
  "src/shared",
17
18
  "src/logger.ts",
18
19
  "README.md"
19
20
  ],
20
21
  "scripts": {
21
- "build": "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk && bun build src/tui/index.tsx --outfile dist/tui.js --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opencode-ai/sdk --external @opentui/solid --external solid-js && tsc --emitDeclarationOnly",
22
- "typecheck": "tsc --noEmit",
22
+ "build": "bun build src/index.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/sdk && tsc --emitDeclarationOnly && bun run build:tui",
23
+ "typecheck": "tsc --noEmit && tsc -p tsconfig.scripts.json",
23
24
  "pretest": "node -e \"if (process.env.CI !== 'true') { const { spawnSync } = require('node:child_process'); const result = spawnSync('cargo', ['build'], { cwd: '../..', stdio: 'inherit', shell: process.platform === 'win32' }); process.exit(result.status ?? 1); } else { console.log('Skipping cargo build in CI; workflow builds agent-file-tools before tests.'); }\"",
24
25
  "test": "bun test",
25
26
  "test:e2e": "bun test src/__tests__/e2e/",
@@ -27,25 +28,27 @@
27
28
  "schema": "bun scripts/build-schema.ts",
28
29
  "build:tool-schemas": "bun run scripts/build-tool-schemas.ts",
29
30
  "prepublishOnly": "bun run build",
30
- "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'"
31
+ "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'",
32
+ "build:tui": "bun scripts/build-tui.ts",
33
+ "smoke:tui": "bun test ./scripts/smoke-tui-pack-install.ts"
31
34
  },
32
35
  "dependencies": {
33
36
  "@clack/prompts": "^1.6.0",
34
- "@cortexkit/aft-bridge": "0.45.1",
35
- "@opentui/core": "^0.4.2",
36
- "@opentui/solid": "^0.4.2",
37
+ "@cortexkit/aft-bridge": "0.46.0",
38
+ "@opentui/core": "0.4.3",
39
+ "@opentui/solid": "0.4.3",
37
40
  "comment-json": "^4.6.2",
38
41
  "solid-js": "1.9.12",
39
42
  "undici": "^7.25.0",
40
43
  "zod": "^4.4.3"
41
44
  },
42
45
  "optionalDependencies": {
43
- "@cortexkit/aft-darwin-arm64": "0.45.1",
44
- "@cortexkit/aft-darwin-x64": "0.45.1",
45
- "@cortexkit/aft-linux-arm64": "0.45.1",
46
- "@cortexkit/aft-linux-x64": "0.45.1",
47
- "@cortexkit/aft-win32-arm64": "0.45.1",
48
- "@cortexkit/aft-win32-x64": "0.45.1"
46
+ "@cortexkit/aft-darwin-arm64": "0.46.0",
47
+ "@cortexkit/aft-darwin-x64": "0.46.0",
48
+ "@cortexkit/aft-linux-arm64": "0.46.0",
49
+ "@cortexkit/aft-linux-x64": "0.46.0",
50
+ "@cortexkit/aft-win32-arm64": "0.46.0",
51
+ "@cortexkit/aft-win32-x64": "0.46.0"
49
52
  },
50
53
  "devDependencies": {
51
54
  "@opencode-ai/plugin": "^1.17.11",
@@ -60,7 +63,7 @@
60
63
  },
61
64
  "./tui": {
62
65
  "types": "./src/tui/index.tsx",
63
- "import": "./src/tui/index.tsx"
66
+ "import": "./src/tui/entry.mjs"
64
67
  }
65
68
  },
66
69
  "oc-plugin": [
@@ -9,13 +9,15 @@ import { resolvePromptContext } from "./last-assistant-model.js";
9
9
  * it shows in the right place in the OpenCode UI, and carries `noReply: true`
10
10
  * so no LLM call is made.
11
11
  *
12
- * IMPORTANT (cache + crash safety): this path deliberately passes ONLY `agent`
13
- * (never model/variant). OpenCode crashes if model/variant are supplied on a
14
- * `noReply: true` prompt, and omitting them keeps the synthetic message from
15
- * busting the provider prefix cache the previous assistant turn warmed.
16
- *
17
- * Lives in `shared/` (not `index.ts`) because `index.ts` must export only the
18
- * plugin default; both the plugin entry and `tools/permissions.ts` call this.
12
+ * Model/variant ARE passed when resolvable, mirroring the session's newest
13
+ * context: OpenCode's `createUserMessage` resolves an omitted model as
14
+ * `agent.model ?? session-current ?? default` and PERSISTS the result via
15
+ * `setAgentModel` — so omitting the model on a session whose user picked a
16
+ * non-default model silently resets the session's model/variant (observed on
17
+ * OpenCode Desktop via /aft-status). Passing the resolved current context
18
+ * makes that persistence a no-op. Older OpenCode builds crashed when model
19
+ * was supplied on a `noReply: true` prompt, so on any failure we retry once
20
+ * without model/variant rather than dropping the message.
19
21
  */
20
22
  export async function sendIgnoredMessage(
21
23
  client: unknown,
@@ -30,32 +32,49 @@ export async function sendIgnoredMessage(
30
32
  };
31
33
 
32
34
  let agent: string | undefined;
35
+ let model: { providerID: string; modelID: string } | undefined;
36
+ let variant: string | undefined;
33
37
  try {
34
38
  const ctx = await resolvePromptContext(
35
39
  client as Parameters<typeof resolvePromptContext>[0],
36
40
  sessionID,
37
41
  );
38
42
  agent = ctx?.agent;
43
+ model = ctx?.model;
44
+ variant = ctx?.variant;
39
45
  } catch {
40
46
  agent = undefined;
41
47
  }
42
48
 
43
- const body: Record<string, unknown> = {
49
+ const send = async (body: Record<string, unknown>): Promise<void> => {
50
+ const promptInput = { path: { id: sessionID }, body };
51
+ if (typeof typedClient.session?.prompt === "function") {
52
+ await Promise.resolve(typedClient.session.prompt(promptInput));
53
+ return;
54
+ }
55
+ if (typeof typedClient.session?.promptAsync === "function") {
56
+ await typedClient.session.promptAsync(promptInput);
57
+ return;
58
+ }
59
+ throw new Error("[aft-plugin] client.session.prompt is unavailable");
60
+ };
61
+
62
+ const base: Record<string, unknown> = {
44
63
  noReply: true,
45
64
  parts: [{ type: "text", text, ignored: true }],
46
65
  };
47
- if (agent) body.agent = agent;
48
- const promptInput = { path: { id: sessionID }, body };
49
-
50
- if (typeof typedClient.session?.prompt === "function") {
51
- await Promise.resolve(typedClient.session.prompt(promptInput));
52
- return;
53
- }
66
+ if (agent) base.agent = agent;
54
67
 
55
- if (typeof typedClient.session?.promptAsync === "function") {
56
- await typedClient.session.promptAsync(promptInput);
57
- return;
68
+ if (model) {
69
+ const withModel: Record<string, unknown> = { ...base, model };
70
+ if (variant) withModel.variant = variant;
71
+ try {
72
+ await send(withModel);
73
+ return;
74
+ } catch {
75
+ // Retry below without model/variant (legacy-host compatibility).
76
+ }
58
77
  }
59
78
 
60
- throw new Error("[aft-plugin] client.session.prompt is unavailable");
79
+ await send(base);
61
80
  }
@@ -25,6 +25,61 @@ export interface AftRpcCallOptions {
25
25
  * placeholder wins.
26
26
  */
27
27
  accept?: (result: unknown) => boolean;
28
+ /**
29
+ * Internal one-hop guard for verified-directory redirects. A redirected
30
+ * call must never redirect again (placeholder chains would otherwise
31
+ * ping-pong between two bridgeless instances).
32
+ */
33
+ noRedirect?: boolean;
34
+ }
35
+
36
+ /**
37
+ * Verified-directory redirect registry (module-level, shared by every client
38
+ * in this process).
39
+ *
40
+ * Why it exists: the TUI process can run in a DIFFERENT directory than the
41
+ * process hosting the session's warm bridge (TUI attached to `opencode
42
+ * serve` / Desktop, launched from $HOME). Port-file discovery is keyed by
43
+ * hash(directory), so the TUI's client only ever finds its own bridgeless
44
+ * instance — which answers the lazy placeholder forever. The placeholder now
45
+ * carries `verified_directory` (the session's SDK-verified project root);
46
+ * `call()` follows it once, to the port files of the instance that actually
47
+ * owns the session's bridge.
48
+ *
49
+ * The registry remembers learned redirects so `resolveEndpoint()` (the
50
+ * persistent notification WebSocket) can aim at the same instance, and
51
+ * notifies listeners so an already-connected socket re-homes instead of
52
+ * staying subscribed to the bridgeless instance that will never push.
53
+ */
54
+ const redirectTargets = new Map<string, string>();
55
+ const redirectListeners = new Set<(from: string, to: string) => void>();
56
+
57
+ export function subscribeRpcRedirects(listener: (from: string, to: string) => void): () => void {
58
+ redirectListeners.add(listener);
59
+ return () => {
60
+ redirectListeners.delete(listener);
61
+ };
62
+ }
63
+
64
+ function recordRedirect(from: string, to: string): void {
65
+ if (redirectTargets.get(from) === to) return;
66
+ redirectTargets.set(from, to);
67
+ for (const listener of redirectListeners) {
68
+ try {
69
+ listener(from, to);
70
+ } catch {
71
+ // one listener must not block the others
72
+ }
73
+ }
74
+ }
75
+
76
+ export function __resetRpcRedirectsForTest(): void {
77
+ redirectTargets.clear();
78
+ redirectListeners.clear();
79
+ }
80
+
81
+ export function __recordRpcRedirectForTest(from: string, to: string): void {
82
+ recordRedirect(from, to);
28
83
  }
29
84
 
30
85
  function abortError(signal: AbortSignal): Error {
@@ -59,8 +114,12 @@ export class AftRpcClient {
59
114
  private portsDir: string;
60
115
  private legacyPortFile: string;
61
116
  private stalePortFailures = new Map<string, number>();
117
+ private storageDir: string;
118
+ private directory: string;
62
119
 
63
120
  constructor(storageDir: string, directory: string) {
121
+ this.storageDir = storageDir;
122
+ this.directory = directory;
64
123
  this.portsDir = rpcPortFileDir(storageDir, directory);
65
124
  this.legacyPortFile = rpcPortFilePath(storageDir, directory);
66
125
  }
@@ -110,12 +169,54 @@ export class AftRpcClient {
110
169
  }
111
170
  }
112
171
 
113
- // All ports returned placeholder OR failed. Use placeholder if we have
114
- // one (sidebar then shows the lazy-spawn UI); otherwise rethrow last error.
115
- if (placeholder !== null) return placeholder;
172
+ // All ports returned placeholder OR failed. Before settling for the
173
+ // placeholder, follow its verified_directory once: the session's warm
174
+ // bridge may live in another process registered under a different
175
+ // directory hash (TUI attached to a serve/Desktop host). See the
176
+ // redirect registry doc above.
177
+ if (placeholder !== null) {
178
+ const redirected = await this.followVerifiedDirectory<T>(
179
+ method,
180
+ params,
181
+ options,
182
+ placeholder,
183
+ );
184
+ if (redirected !== null) return redirected;
185
+ return placeholder;
186
+ }
116
187
  throw lastError instanceof Error ? lastError : new Error(String(lastError));
117
188
  }
118
189
 
190
+ /**
191
+ * One-hop redirect to the instance that owns the session's bridge. Returns
192
+ * null (caller keeps the placeholder) unless the redirected call produced a
193
+ * warm response — a placeholder from the target is not an improvement, and
194
+ * failures must not mask the original placeholder.
195
+ */
196
+ private async followVerifiedDirectory<T>(
197
+ method: string,
198
+ params: Record<string, unknown>,
199
+ options: AftRpcCallOptions,
200
+ placeholder: T,
201
+ ): Promise<T | null> {
202
+ if (options.noRedirect) return null;
203
+ const verified = (placeholder as Record<string, unknown> | null)?.verified_directory;
204
+ if (typeof verified !== "string" || verified.length === 0 || verified === this.directory) {
205
+ return null;
206
+ }
207
+ const target = new AftRpcClient(this.storageDir, verified);
208
+ try {
209
+ const result = await target.call<T>(method, params, { ...options, noRedirect: true });
210
+ if (this.looksLikePlaceholder(result)) return null;
211
+ // Only remember (and re-home the notification socket) once the target
212
+ // actually served warm data — a dead redirect must stay unlearned.
213
+ recordRedirect(this.directory, verified);
214
+ return result;
215
+ } catch {
216
+ return null;
217
+ }
218
+ }
219
+
119
220
  private async callOne<T>(
120
221
  method: string,
121
222
  params: Record<string, unknown>,
@@ -159,8 +260,19 @@ export class AftRpcClient {
159
260
  }
160
261
  }
161
262
 
162
- /** Resolve the freshest live endpoint for the persistent TUI WebSocket. */
263
+ /** Resolve the freshest live endpoint for the persistent TUI WebSocket.
264
+ * Follows a learned verified-directory redirect so the socket subscribes
265
+ * to the instance that owns the session's bridge (the one that will
266
+ * actually push status-changed frames), not this directory's bridgeless
267
+ * placeholder instance. */
163
268
  async resolveEndpoint(signal?: AbortSignal): Promise<AftRpcEndpoint | null> {
269
+ const redirect = redirectTargets.get(this.directory);
270
+ if (redirect) {
271
+ const target = new AftRpcClient(this.storageDir, redirect);
272
+ const [info] = await target.resolvePortInfos(signal);
273
+ if (info) return { port: info.port, token: info.token };
274
+ // Redirect target gone (process exited) — fall through to own ports.
275
+ }
164
276
  const [info] = await this.resolvePortInfos(signal);
165
277
  if (!info) return null;
166
278
  return { port: info.port, token: info.token };
@@ -0,0 +1,16 @@
1
+ // Prefer the host OpenTUI runtime registry when it exists. OpenTUI 0.4.x
2
+ // registers these virtual modules process-wide, which lets the precompiled TUI
3
+ // use the host's single Solid/OpenTUI runtime even when this package is loaded
4
+ // from an npm cache under node_modules.
5
+ let mod;
6
+ try {
7
+ await import("opentui:runtime-module:" + encodeURIComponent("@opentui/solid"));
8
+ mod = await import("../tui-compiled/index.tsx");
9
+ } catch {
10
+ // Older hosts and bare Bun do not provide the virtual registry. Falling back
11
+ // to the raw TSX entry keeps development checkouts and OpenTUI 0.3.x hosts
12
+ // working where the Solid transform still applies to this source path.
13
+ mod = await import("./index.tsx");
14
+ }
15
+
16
+ export default mod.default;
@@ -1,4 +1,4 @@
1
- import { AftRpcClient, type AftRpcEndpoint } from "../shared/rpc-client";
1
+ import { AftRpcClient, type AftRpcEndpoint, subscribeRpcRedirects } from "../shared/rpc-client";
2
2
  import { resolveCortexKitStorageRoot } from "../shared/storage-paths";
3
3
 
4
4
  export interface SocketNotification {
@@ -86,14 +86,41 @@ export function startAftTuiSocket(options: TuiSocketOptions): void {
86
86
  opts = options;
87
87
  closed = false;
88
88
  generation += 1;
89
+ ensureRedirectSubscription();
89
90
  void reconcileSocketScope();
90
91
  }
91
92
 
93
+ /**
94
+ * Re-home the socket when a status call learns a verified-directory redirect
95
+ * (see rpc-client). The socket may already be connected to this directory's
96
+ * bridgeless instance — which authenticates and accepts hello but will never
97
+ * push a status-changed frame, because the session's bridge lives in another
98
+ * process. Drop it and reconnect; resolveEndpoint now follows the redirect.
99
+ */
100
+ let redirectUnsubscribe: (() => void) | null = null;
101
+ function ensureRedirectSubscription(): void {
102
+ if (redirectUnsubscribe) return;
103
+ redirectUnsubscribe = subscribeRpcRedirects((from) => {
104
+ if (closed) return;
105
+ const scope = currentScope();
106
+ if (!scope || scope.directory !== from) return;
107
+ generation += 1;
108
+ connectingGeneration = null;
109
+ connectingScope = null;
110
+ closeCurrentSocket(false);
111
+ void connect(scope, generation);
112
+ });
113
+ }
114
+
92
115
  export function stopAftTuiSocket(): void {
93
116
  closed = true;
94
117
  generation += 1;
95
118
  connectingGeneration = null;
96
119
  connectingScope = null;
120
+ if (redirectUnsubscribe) {
121
+ redirectUnsubscribe();
122
+ redirectUnsubscribe = null;
123
+ }
97
124
  if (reconnectTimer) {
98
125
  deps.clearTimeout(reconnectTimer);
99
126
  reconnectTimer = undefined;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Pick the text color for a sidebar badge label drawn on a theme accent.
3
+ * Keep this logic in sync with Magic Context so both sidebars make the same
4
+ * contrast decision for shared themes.
5
+ */
6
+
7
+ type Color = { r: number; g: number; b: number; a?: number };
8
+
9
+ const MIN_OPAQUE_ALPHA = 0.5;
10
+ const MIN_CHANNEL_DISTANCE = 0.06;
11
+ const LIGHT_ACCENT_LUMINANCE = 0.5;
12
+
13
+ function srgbChannelToLinear(c: number): number {
14
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
15
+ }
16
+
17
+ function relativeLuminance(bg: Color): number {
18
+ return (
19
+ 0.2126 * srgbChannelToLinear(bg.r) +
20
+ 0.7152 * srgbChannelToLinear(bg.g) +
21
+ 0.0722 * srgbChannelToLinear(bg.b)
22
+ );
23
+ }
24
+
25
+ function nearlyEqual(a: Color, b: Color): boolean {
26
+ return (
27
+ Math.abs(a.r - b.r) < MIN_CHANNEL_DISTANCE &&
28
+ Math.abs(a.g - b.g) < MIN_CHANNEL_DISTANCE &&
29
+ Math.abs(a.b - b.b) < MIN_CHANNEL_DISTANCE
30
+ );
31
+ }
32
+
33
+ export function readableTextColorOn(bg: Color): string {
34
+ return relativeLuminance(bg) < LIGHT_ACCENT_LUMINANCE ? "#ffffff" : "#000000";
35
+ }
36
+
37
+ export function badgeTextColor<T extends Color>(accent: T, background: T): T | string {
38
+ const alpha = background.a ?? 1;
39
+ if (alpha >= MIN_OPAQUE_ALPHA && !nearlyEqual(accent, background)) {
40
+ return background;
41
+ }
42
+ return readableTextColorOn(accent);
43
+ }