@cortexkit/aft-opencode 0.58.2 → 0.59.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.
Files changed (85) hide show
  1. package/dist/bash-wait-detach.d.ts.map +1 -1
  2. package/dist/bg-notifications.d.ts +8 -0
  3. package/dist/bg-notifications.d.ts.map +1 -1
  4. package/dist/bridge-bootstrap.d.ts +4 -4
  5. package/dist/config-live-reload.d.ts +36 -8
  6. package/dist/config-live-reload.d.ts.map +1 -1
  7. package/dist/config.d.ts +72 -13
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/configure-warnings.d.ts.map +1 -1
  10. package/dist/entry/server.js +3738 -1780
  11. package/dist/hooks/auto-update-checker/checker.d.ts.map +1 -1
  12. package/dist/hooks/auto-update-checker/index.d.ts.map +1 -1
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +3580 -1817
  15. package/dist/lsp-auto-install.d.ts.map +1 -1
  16. package/dist/lsp-github-install.d.ts +5 -2
  17. package/dist/lsp-github-install.d.ts.map +1 -1
  18. package/dist/notifications.d.ts +1 -1
  19. package/dist/notifications.d.ts.map +1 -1
  20. package/dist/rpc/contract.d.ts +1 -1
  21. package/dist/rpc/contract.d.ts.map +1 -1
  22. package/dist/rpc/register.d.ts +3 -1
  23. package/dist/rpc/register.d.ts.map +1 -1
  24. package/dist/shared/ignored-message.d.ts.map +1 -1
  25. package/dist/shared/read-vision.d.ts.map +1 -1
  26. package/dist/shared/rpc-client.d.ts +8 -0
  27. package/dist/shared/rpc-client.d.ts.map +1 -1
  28. package/dist/shared/rpc-server.d.ts.map +1 -1
  29. package/dist/shared/session-directory.d.ts +7 -5
  30. package/dist/shared/session-directory.d.ts.map +1 -1
  31. package/dist/shared/status.d.ts +1 -0
  32. package/dist/shared/status.d.ts.map +1 -1
  33. package/dist/shared/v2-context.d.ts +3 -0
  34. package/dist/shared/v2-context.d.ts.map +1 -0
  35. package/dist/subc-tool-schemas.d.ts +23 -1
  36. package/dist/subc-tool-schemas.d.ts.map +1 -1
  37. package/dist/tool-registration.d.ts +3 -3
  38. package/dist/tool-registration.d.ts.map +1 -1
  39. package/dist/tools/_shared.d.ts.map +1 -1
  40. package/dist/tools/bash.d.ts +17 -0
  41. package/dist/tools/bash.d.ts.map +1 -1
  42. package/dist/tools/bash_watch.d.ts +11 -1
  43. package/dist/tools/bash_watch.d.ts.map +1 -1
  44. package/dist/tools/definitions/v2.d.ts +2 -0
  45. package/dist/tools/definitions/v2.d.ts.map +1 -1
  46. package/dist/tools/hoisted.d.ts +18 -0
  47. package/dist/tools/hoisted.d.ts.map +1 -1
  48. package/dist/tools/inspect.d.ts +10 -1
  49. package/dist/tools/inspect.d.ts.map +1 -1
  50. package/dist/tools/permissions.d.ts +2 -1
  51. package/dist/tools/permissions.d.ts.map +1 -1
  52. package/dist/tools/search.d.ts.map +1 -1
  53. package/dist/tools/semantic.d.ts.map +1 -1
  54. package/dist/tui/aft-rpc.d.ts +14 -0
  55. package/dist/tui/aft-rpc.d.ts.map +1 -1
  56. package/dist/tui/v2-status.d.ts.map +1 -1
  57. package/dist/v2-host-tool-overlap.d.ts +66 -0
  58. package/dist/v2-host-tool-overlap.d.ts.map +1 -0
  59. package/dist/v2-prompt-detach.d.ts +1 -0
  60. package/dist/v2-prompt-detach.d.ts.map +1 -1
  61. package/dist/v2-tool-hooks.d.ts +6 -0
  62. package/dist/v2-tool-hooks.d.ts.map +1 -0
  63. package/dist/v2-workflow-hints.d.ts +4 -0
  64. package/dist/v2-workflow-hints.d.ts.map +1 -0
  65. package/dist/wakes/runtime-consumer.d.ts.map +1 -1
  66. package/dist/wakes/session-delivery.d.ts.map +1 -1
  67. package/package.json +9 -9
  68. package/src/entry/server-runtime.mjs +21 -1
  69. package/src/shared/ignored-message.ts +18 -0
  70. package/src/shared/read-vision.ts +14 -4
  71. package/src/shared/rpc-client.ts +46 -5
  72. package/src/shared/rpc-server.ts +10 -0
  73. package/src/shared/session-directory.ts +67 -19
  74. package/src/shared/status.ts +15 -5
  75. package/src/shared/v2-context.ts +7 -0
  76. package/src/tui/aft-rpc.ts +11 -0
  77. package/src/tui/index.tsx +20 -16
  78. package/src/tui/sidebar-view.tsx +9 -1
  79. package/src/tui/v2-status.ts +7 -5
  80. package/src/tui/v2.tsx +47 -0
  81. package/src/tui-compiled/aft-rpc.ts +11 -0
  82. package/src/tui-compiled/index.tsx +27 -16
  83. package/src/tui-compiled/sidebar-view.tsx +11 -2
  84. package/src/tui-compiled/v2-status.ts +7 -5
  85. package/src/tui-compiled/v2.tsx +26 -0
@@ -0,0 +1,66 @@
1
+ /**
2
+ * On OpenCode 2, AFT's `apply_patch` and `bash` sit beside the host's own
3
+ * `patch` and `shell` tools unless the user's opencode config turns the host
4
+ * plugins off (`-opencode.tool.patch` / `-opencode.tool.shell` under
5
+ * `plugins`, which `aft setup` and `aft doctor --fix` write). Someone who
6
+ * installed AFT without setup, or turned the host tools back on, gets two
7
+ * editing tools and two shells with nothing saying why. This module tells the
8
+ * user once per host process and points at `doctor --fix`.
9
+ *
10
+ * What counts is what the host actually registered: `context.tool.list()`
11
+ * returns the host's tools after every plugin's transform, keyed by the name
12
+ * the model sees. The check waits for the first user prompt rather than
13
+ * running at plugin start, because at start other plugins (the host's own
14
+ * patch and shell plugins included) may not have registered yet, and because
15
+ * the notice is an RPC event the host does not replay: a TUI that has not
16
+ * subscribed yet would miss it, while a prompt typed into the TUI proves its
17
+ * subscription is live. A host without `tool.list()`
18
+ * (OpenCode 2.0.3 has none) or a list call that fails gets no notice: the
19
+ * reason is logged once at debug level and nothing is guessed.
20
+ *
21
+ * The notice must reach the user and never the model. Nothing is written into
22
+ * the session: OpenCode 2 sends a synthetic session record to the model as a
23
+ * user message on every later turn, where a notice would read as an
24
+ * instruction. A server plugin has no toast or dialog of its own, so the
25
+ * notice goes out as an AFT RPC event and AFT's OpenCode 2 TUI plugin shows
26
+ * it as a toast. The desktop and web apps run no TUI plugin and show nothing;
27
+ * `aft doctor` reports the same problem there.
28
+ */
29
+ import { Effect } from "effect";
30
+ /** An AFT tool and the OpenCode 2 built-in that does the same job. */
31
+ export interface HostToolOverlap {
32
+ readonly aft: "apply_patch" | "bash";
33
+ readonly host: "patch" | "shell";
34
+ }
35
+ /**
36
+ * The pairs where AFT registered its tool (it is not in `disabled_tools`) and
37
+ * the host also has its built-in, in a fixed order. `hostToolNames` is the
38
+ * host's whole registry after every transform, AFT's own tools included, so a
39
+ * name AFT registered itself is never taken for the host's built-in.
40
+ */
41
+ export declare function findHostToolOverlaps(hostToolNames: ReadonlySet<string>, registeredAftTools: ReadonlySet<string>): HostToolOverlap[];
42
+ /** One notice for every overlapping pair, or null when there is none. */
43
+ export declare function hostToolOverlapNotice(overlaps: readonly HostToolOverlap[]): string | null;
44
+ /** Reset the once-per-process state. Tests only. */
45
+ export declare function __resetHostToolOverlapNoticeForTests(): void;
46
+ interface PromptEvent {
47
+ readonly sessionID: string;
48
+ readonly metadata?: Record<string, unknown>;
49
+ }
50
+ /** Hands the notice text to the user-only channel (the TUI toast event). */
51
+ export type HostToolOverlapDelivery = (message: string) => Promise<void>;
52
+ /**
53
+ * The session `prompt` hook for one Location. It never fails and never waits:
54
+ * the host holds the prompt until the hook returns, so the check runs in the
55
+ * background after it.
56
+ */
57
+ export declare function createHostToolOverlapPromptHook(context: unknown, registeredAftTools: ReadonlySet<string>, deliver: HostToolOverlapDelivery, onChecked?: (done: Promise<void>) => void): (event: PromptEvent) => Effect.Effect<void>;
58
+ /**
59
+ * Register the overlap check on an OpenCode 2 host's session `prompt` hook.
60
+ * `registeredAftTools` is AFT's final tool surface, so a tool listed in
61
+ * `disabled_tools` never counts as an overlap. `deliver` must reach the user
62
+ * only, never the session.
63
+ */
64
+ export declare function registerV2HostToolOverlapNotice(context: unknown, registeredAftTools: ReadonlySet<string>, deliver: HostToolOverlapDelivery, onChecked?: (done: Promise<void>) => void): Effect.Effect<boolean, never, unknown>;
65
+ export {};
66
+ //# sourceMappingURL=v2-host-tool-overlap.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-host-tool-overlap.d.ts","sourceRoot":"","sources":["../src/v2-host-tool-overlap.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAKhC,sEAAsE;AACtE,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,aAAa,GAAG,MAAM,CAAC;IACrC,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;CAClC;AASD;;;;;GAKG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,WAAW,CAAC,MAAM,CAAC,EAClC,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,GACtC,eAAe,EAAE,CAOnB;AAED,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,SAAS,eAAe,EAAE,GAAG,MAAM,GAAG,IAAI,CAazF;AAQD,oDAAoD;AACpD,wBAAgB,oCAAoC,IAAI,IAAI,CAG3D;AAwDD,UAAU,WAAW;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC7C;AAED,4EAA4E;AAC5E,MAAM,MAAM,uBAAuB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;AAgCzE;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,EACvC,OAAO,EAAE,uBAAuB,EAChC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,GACxC,CAAC,KAAK,EAAE,WAAW,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAe7C;AAOD;;;;;GAKG;AACH,wBAAgB,+BAA+B,CAC7C,OAAO,EAAE,OAAO,EAChB,kBAAkB,EAAE,WAAW,CAAC,MAAM,CAAC,EACvC,OAAO,EAAE,uBAAuB,EAChC,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,CAAC,IAAI,CAAC,KAAK,IAAI,GACxC,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAkBxC"}
@@ -35,6 +35,7 @@ export interface V2PromptDetachRuntime {
35
35
  * `bash.detach_on_user_message` applies to the next message.
36
36
  */
37
37
  getConfig(): AftConfig;
38
+ registerSession?: (sessionID: string) => void;
38
39
  }
39
40
  /**
40
41
  * Run the shared chat-message decision on one OpenCode 2 prompt and write the
@@ -1 +1 @@
1
- {"version":3,"file":"v2-prompt-detach.d.ts","sourceRoot":"","sources":["../src/v2-prompt-detach.ts"],"names":[],"mappings":"AAaA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,8DAA8D;AAC9D,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,0FAA0F;AAC1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;QACzB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;QAC1B,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,gCAAgC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9E,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,SAAS,IAAI,SAAS,CAAC;CACxB;AAuDD;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,qBAAqB,EAC9B,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAuBT;AAMD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,GAC7B,CAAC,KAAK,EAAE,iBAAiB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAenD;AAOD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,qBAAqB,GAC7B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAYxC"}
1
+ {"version":3,"file":"v2-prompt-detach.d.ts","sourceRoot":"","sources":["../src/v2-prompt-detach.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC,OAAO,EAAE,gCAAgC,EAAE,MAAM,uBAAuB,CAAC;AACzE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAI7C,8DAA8D;AAC9D,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;CACd;AAED,UAAU,cAAc;IACtB,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,0FAA0F;AAC1F,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,MAAM,EAAE;QACN,IAAI,EAAE,MAAM,CAAC;QACb,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;QACzB,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;QAC1B,MAAM,CAAC,EAAE,cAAc,EAAE,CAAC;KAC3B,CAAC;IACF,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,KAAK,eAAe,GAAG,UAAU,CAAC,OAAO,gCAAgC,CAAC,CAAC,CAAC,CAAC,CAAC;AAE9E,MAAM,WAAW,qBAAqB;IACpC,gEAAgE;IAChE,QAAQ,CAAC,IAAI,EAAE,eAAe,CAAC;IAC/B;;;OAGG;IACH,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B;;;OAGG;IACH,SAAS,IAAI,SAAS,CAAC;IACvB,eAAe,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;CAC/C;AAqCD;;;;;;;;;;;;GAYG;AACH,wBAAgB,6BAA6B,CAC3C,OAAO,EAAE,qBAAqB,EAC9B,KAAK,EAAE,iBAAiB,GACvB,OAAO,CAuBT;AAMD;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,qBAAqB,GAC7B,CAAC,KAAK,EAAE,iBAAiB,KAAK,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,CAgBnD;AAOD;;;;;GAKG;AACH,wBAAgB,0BAA0B,CACxC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,qBAAqB,GAC7B,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,CAAC,CAYxC"}
@@ -0,0 +1,6 @@
1
+ import { Effect } from "effect";
2
+ /** Port the V1 raw-argument, result-text and current-model seams to V2. */
3
+ export declare function registerV2ToolHooks(context: unknown, runtime: {
4
+ hashlineEffective?: boolean;
5
+ }, registeredTools: ReadonlySet<string>): Effect.Effect<void, never, import("effect/Scope").Scope>;
6
+ //# sourceMappingURL=v2-tool-hooks.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-tool-hooks.d.ts","sourceRoot":"","sources":["../src/v2-tool-hooks.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,EAAU,MAAM,QAAQ,CAAC;AAqBxC,2EAA2E;AAC3E,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE;IAAE,iBAAiB,CAAC,EAAE,OAAO,CAAA;CAAE,EACxC,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,4DA6DrC"}
@@ -0,0 +1,4 @@
1
+ import { Effect } from "effect";
2
+ /** V2 consumes session system hooks, not V1's chat.system.transform. */
3
+ export declare function registerV2WorkflowHints(context: unknown, hintsBlock: string | null): Effect.Effect<boolean, never, import("effect/Scope").Scope>;
4
+ //# sourceMappingURL=v2-workflow-hints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"v2-workflow-hints.d.ts","sourceRoot":"","sources":["../src/v2-workflow-hints.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAIhC,wEAAwE;AACxE,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,IAAI,+DAelF"}
@@ -1 +1 @@
1
- {"version":3,"file":"runtime-consumer.d.ts","sourceRoot":"","sources":["../../src/wakes/runtime-consumer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,aAAa,EACd,MAAM,uBAAuB,CAAC;AAM/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,uBAAuB,CAAC;AAEnF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;CACtC;AAkJD,mFAAmF;AACnF,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED,QAAA,MAAM,mBAAmB,EAAE,IAAI,CAAC,aAAa,EAAE,kBAAkB,GAAG,mBAAmB,CAGtF,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,QAAQ,CAAC,aAAa,EAAE,OAAO,mBAAmB,CAAC;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAiErF"}
1
+ {"version":3,"file":"runtime-consumer.d.ts","sourceRoot":"","sources":["../../src/wakes/runtime-consumer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAIV,aAAa,EACd,MAAM,uBAAuB,CAAC;AAM/B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,4BAA4B,CAAC;AAClE,OAAO,EAAE,KAAK,kBAAkB,EAAqB,MAAM,uBAAuB,CAAC;AAEnF,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,OAAO,EAAE,kBAAkB,CAAC;CACtC;AAyJD,mFAAmF;AACnF,wBAAgB,2BAA2B,IAAI,IAAI,CAKlD;AAED,QAAA,MAAM,mBAAmB,EAAE,IAAI,CAAC,aAAa,EAAE,kBAAkB,GAAG,mBAAmB,CAGtF,CAAC;AAEF,MAAM,WAAW,iBAAkB,SAAQ,eAAe;IACxD,QAAQ,CAAC,aAAa,EAAE,OAAO,mBAAmB,CAAC;IACnD,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,iBAAiB,GAAG,iBAAiB,CAiErF"}
@@ -1 +1 @@
1
- {"version":3,"file":"session-delivery.d.ts","sourceRoot":"","sources":["../../src/wakes/session-delivery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAEhC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAMhE;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACrE,SAAS,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAIhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAS;gBAEI,OAAO,EAAE,kBAAkB;IAExD,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;IA0BlE,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC;IAe7D,OAAO,IAAI,IAAI;CAKhB"}
1
+ {"version":3,"file":"session-delivery.d.ts","sourceRoot":"","sources":["../../src/wakes/session-delivery.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAQ,MAAM,QAAQ,CAAC;AAEtC;;;;;GAKG;AACH,eAAO,MAAM,iBAAiB,QAAQ,CAAC;AAEvC,yEAAyE;AACzE,wBAAgB,qBAAqB,CAAC,QAAQ,EAAE,OAAO,GAAG,OAAO,CAMhE;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC;CACxB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,CAAC,KAAK,EAAE,oBAAoB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACrE,SAAS,CAAC,KAAK,EAAE,uBAAuB,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;CAC5E;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;CACvD;AAED;;;GAGG;AACH,qBAAa,iBAAiB;IAIhB,OAAO,CAAC,QAAQ,CAAC,OAAO;IAHpC,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAqB;IACzD,OAAO,CAAC,QAAQ,CAAS;gBAEI,OAAO,EAAE,kBAAkB;IAExD,UAAU,CAAC,KAAK,EAAE,cAAc,GAAG,MAAM,CAAC,MAAM,CAAC,OAAO,EAAE,OAAO,CAAC;IAqClE,MAAM,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC;IAe7D,OAAO,IAAI,IAAI;CAKhB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/aft-opencode",
3
- "version": "0.58.2",
3
+ "version": "0.59.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",
@@ -33,13 +33,13 @@
33
33
  "schema": "bun scripts/build-schema.ts",
34
34
  "build:tool-schemas": "bun run scripts/build-tool-schemas.ts",
35
35
  "prepublishOnly": "bun run build",
36
- "test:unit": "bun test src/__tests__ --path-ignore-patterns 'src/__tests__/e2e/**'",
36
+ "test:unit": "bun test src/__tests__ ./test/ ./__tests__/ --path-ignore-patterns 'src/__tests__/e2e/**'",
37
37
  "build:tui": "bun scripts/build-tui.ts",
38
38
  "smoke:tui": "bun test ./scripts/smoke-tui-pack-install.ts"
39
39
  },
40
40
  "dependencies": {
41
41
  "@clack/prompts": "^1.6.0",
42
- "@cortexkit/aft-bridge": "0.58.2",
42
+ "@cortexkit/aft-bridge": "0.59.0",
43
43
  "@opencode/client": "2.0.11",
44
44
  "@opentui/core": "0.4.3",
45
45
  "@opentui/solid": "0.4.3",
@@ -50,12 +50,12 @@
50
50
  "zod": "^4.4.3"
51
51
  },
52
52
  "optionalDependencies": {
53
- "@cortexkit/aft-darwin-arm64": "0.58.2",
54
- "@cortexkit/aft-darwin-x64": "0.58.2",
55
- "@cortexkit/aft-linux-arm64": "0.58.2",
56
- "@cortexkit/aft-linux-x64": "0.58.2",
57
- "@cortexkit/aft-win32-arm64": "0.58.2",
58
- "@cortexkit/aft-win32-x64": "0.58.2"
53
+ "@cortexkit/aft-darwin-arm64": "0.59.0",
54
+ "@cortexkit/aft-darwin-x64": "0.59.0",
55
+ "@cortexkit/aft-linux-arm64": "0.59.0",
56
+ "@cortexkit/aft-linux-x64": "0.59.0",
57
+ "@cortexkit/aft-win32-arm64": "0.59.0",
58
+ "@cortexkit/aft-win32-x64": "0.59.0"
59
59
  },
60
60
  "devDependencies": {
61
61
  "@opencode-ai/plugin": "^1.17.11",
@@ -18,8 +18,12 @@ import { debug, log, warn } from "../logger.js";
18
18
  import { resolvePluginVersion } from "../plugin-version.js";
19
19
  import { registerAftConfigErrorRpc, registerAftRpc } from "../rpc/register.js";
20
20
  import { hoistedV2ToolConsumers, v2PromptChannelFor } from "../tools/hoisted/v2.js";
21
+ import { registerV2HostToolOverlapNotice } from "../v2-host-tool-overlap.js";
21
22
  import { registerV2PromptDetachHook } from "../v2-prompt-detach.js";
23
+ import { registerV2ToolHooks } from "../v2-tool-hooks.js";
24
+ import { registerV2WorkflowHints } from "../v2-workflow-hints.js";
22
25
  import { createV2RuntimeConsumer } from "../wakes/runtime-consumer.js";
26
+ import { buildHintsForRegisteredTools } from "../workflow-hints.js";
23
27
  import {
24
28
  buildAftToolDefinitions,
25
29
  openCodeHashlineEffective,
@@ -36,6 +40,9 @@ const defaults = {
36
40
  registerRpc: registerAftRpc,
37
41
  registerConfigErrorRpc: registerAftConfigErrorRpc,
38
42
  registerPromptHook: registerV2PromptDetachHook,
43
+ registerToolHooks: registerV2ToolHooks,
44
+ registerWorkflowHints: registerV2WorkflowHints,
45
+ registerHostToolOverlapNotice: registerV2HostToolOverlapNotice,
39
46
  // The prompt server comes from the user-only `opencode` block of the
40
47
  // Location's config; without it AFT finds the server it runs inside.
41
48
  toolConsumers: (context, config) => ({
@@ -119,6 +126,7 @@ async function bootLocation(context, location, dependencies) {
119
126
  const { hashlineEditRegistered } = applyToolSurfaceOverrides(pool, config, registeredTools);
120
127
  reportHashlineDowngrade(config, registeredTools, notify);
121
128
  toolContext.hashlineEffective = hashlineEditRegistered;
129
+ const hintsBlock = buildHintsForRegisteredTools(config, registeredTools, hashlineEditRegistered);
122
130
  // Keep this Location's live config keys current when a config file changes.
123
131
  // The finalizer that releases the bridge also stops the watch.
124
132
  const liveConfigReload = dependencies.startLiveConfigReload({
@@ -131,7 +139,7 @@ async function bootLocation(context, location, dependencies) {
131
139
  },
132
140
  notify,
133
141
  });
134
- return { consumers, pool, tools, liveConfigReload, toolContext, directory };
142
+ return { consumers, pool, tools, liveConfigReload, toolContext, directory, hintsBlock };
135
143
  }
136
144
 
137
145
  /**
@@ -221,6 +229,7 @@ export function makeServerEffect(overrides = {}) {
221
229
  pool: runtime.pool,
222
230
  projectRoot: runtime.directory,
223
231
  getConfig: () => runtime.toolContext.config,
232
+ registerSession: runtime.consumers.registerSession,
224
233
  });
225
234
  yield* dependencies.registerTools(
226
235
  context,
@@ -228,6 +237,17 @@ export function makeServerEffect(overrides = {}) {
228
237
  runtime.tools,
229
238
  runtime.consumers,
230
239
  );
240
+ yield* dependencies.registerToolHooks(context, runtime.toolContext, new Set(Object.keys(runtime.tools)));
241
+ yield* dependencies.registerWorkflowHints(context, runtime.hintsBlock);
242
+ // Tell the user once when the host's own patch or shell tool still runs
243
+ // beside AFT's apply_patch or bash. Checked on the first prompt, once
244
+ // every plugin has registered its tools, and shown by AFT's TUI plugin
245
+ // as a toast; nothing is written into the session, which the model reads.
246
+ yield* dependencies.registerHostToolOverlapNotice(
247
+ context,
248
+ new Set(Object.keys(runtime.tools)),
249
+ (message) => rpc.emitHostToolOverlap({ message }),
250
+ );
231
251
  });
232
252
  };
233
253
  }
@@ -1,4 +1,7 @@
1
+ import { Effect } from "effect";
2
+
1
3
  import { resolvePromptContext } from "./last-assistant-model.js";
4
+ import { isV2PluginContext } from "./v2-context.js";
2
5
 
3
6
  /**
4
7
  * Append an `ignored: true` synthetic user message to a session.
@@ -24,6 +27,21 @@ export async function sendIgnoredMessage(
24
27
  sessionID: string,
25
28
  text: string,
26
29
  ): Promise<void> {
30
+ if (isV2PluginContext(client)) {
31
+ const context = client as {
32
+ session: {
33
+ synthetic(input: {
34
+ sessionID: string;
35
+ text: string;
36
+ resume: false;
37
+ }): Effect.Effect<unknown, unknown>;
38
+ };
39
+ };
40
+ // V2 owns the current agent/model itself. Synthetic inbox entries neither
41
+ // switch that context nor resume a turn, and must actually run as Effects.
42
+ await Effect.runPromise(context.session.synthetic({ sessionID, text, resume: false }));
43
+ return;
44
+ }
27
45
  const typedClient = client as {
28
46
  session?: {
29
47
  prompt?: (input: unknown) => unknown;
@@ -1,4 +1,7 @@
1
+ import { Effect } from "effect";
2
+
1
3
  import { resolvePromptContext } from "./last-assistant-model.js";
4
+ import { isV2PluginContext } from "./v2-context.js";
2
5
 
3
6
  export const VISION_HOST_TIMEOUT_MS = 1500;
4
7
  type Model = { providerID: string; modelID: string };
@@ -69,12 +72,19 @@ export async function currentSessionVisionCapability(
69
72
  let capability = state.capabilities.get(key);
70
73
  if (!capability) {
71
74
  capability = (async () => {
72
- const api = (client as { provider?: { list?: () => Promise<unknown> } }).provider;
75
+ const api = (client as { provider?: { list?: () => unknown } }).provider;
73
76
  if (!api?.list) return undefined;
74
- const list = api.list.bind(api);
77
+ const list = async () => {
78
+ const result = api.list?.();
79
+ return isV2PluginContext(client)
80
+ ? Effect.runPromise(result as Effect.Effect<unknown, unknown>)
81
+ : await result;
82
+ };
75
83
  const listed = record(await bounded(list));
76
84
  const catalog = record(listed?.data) ?? listed;
77
- const providers = catalog?.all ?? catalog?.providers;
85
+ const providers = Array.isArray(listed?.data)
86
+ ? listed.data
87
+ : (catalog?.all ?? catalog?.providers);
78
88
  if (!Array.isArray(providers)) return undefined;
79
89
  const provider = providers.map(record).find((entry) => entry?.id === currentModel.providerID);
80
90
  const models = provider?.models;
@@ -83,7 +93,7 @@ export async function currentSessionVisionCapability(
83
93
  ? models.find((entry) => record(entry)?.id === currentModel.modelID)
84
94
  : record(models)?.[currentModel.modelID],
85
95
  );
86
- const inputs = record(entry?.modalities)?.input;
96
+ const inputs = record(entry?.capabilities)?.input ?? record(entry?.modalities)?.input;
87
97
  return Array.isArray(inputs)
88
98
  ? inputs.includes("image")
89
99
  : typeof entry?.attachment === "boolean"
@@ -133,19 +133,46 @@ export class AftRpcClient {
133
133
  const { signal } = options;
134
134
  throwIfAborted(signal);
135
135
 
136
+ // Fast path: reuse the endpoint that served the last warm response while
137
+ // it is still the newest live port file, skipping discovery's sequential
138
+ // health checks. Anything other than an accepted warm response falls back
139
+ // to the full scan below.
140
+ let placeholder: T | null = null;
141
+ let lastError: unknown = null;
142
+ let answeredPort: number | null = null;
143
+ const cached = this.cachedEndpoint();
144
+ if (cached) {
145
+ try {
146
+ const result = await this.callOne<T>(method, params, cached, signal);
147
+ if (this.looksLikePlaceholder(result)) {
148
+ placeholder = result;
149
+ answeredPort = cached.port;
150
+ } else if (!options.accept || options.accept(result)) {
151
+ return result;
152
+ } else {
153
+ answeredPort = cached.port;
154
+ }
155
+ } catch {
156
+ throwIfAborted(signal);
157
+ }
158
+ this.port = null;
159
+ this.token = null;
160
+ }
161
+
136
162
  // Try ALL discovered ports for this project (OpenCode TUI under --port 0
137
163
  // loads our plugin twice in the same process, so two RPC servers listen
138
- // and we have to try both — only one's bridge is actually warm).
139
- const infos = await this.resolvePortInfos(signal);
140
- if (infos.length === 0) {
164
+ // and we have to try both — only one's bridge is actually warm). A port
165
+ // that already answered on the fast path is not asked again.
166
+ const infos = (await this.resolvePortInfos(signal)).filter(
167
+ (info) => info.port !== answeredPort,
168
+ );
169
+ if (infos.length === 0 && placeholder === null) {
141
170
  throw new Error("AFT RPC server not available");
142
171
  }
143
172
 
144
173
  // First pass: try every port. Prefer responses that look like "warm
145
174
  // bridge" output (i.e. not the synthetic `status: "not_initialized"`
146
175
  // placeholder served when this instance's bridge hasn't been spawned).
147
- let placeholder: T | null = null;
148
- let lastError: unknown = null;
149
176
  for (const info of infos) {
150
177
  throwIfAborted(signal);
151
178
  try {
@@ -250,6 +277,20 @@ export class AftRpcClient {
250
277
  return status === "not_initialized";
251
278
  }
252
279
 
280
+ /**
281
+ * The endpoint that served the last warm response, if it is still the newest
282
+ * live port file for this project. Discovery tries ports newest first and
283
+ * returns the first warm answer, so while this holds the full scan would
284
+ * reach the same server first. Reading the port files also drops entries
285
+ * whose owning process has died, so a dead server is never reused.
286
+ */
287
+ private cachedEndpoint(): PortInfo | null {
288
+ if (this.port === null) return null;
289
+ const [newest] = this.readAllPortFiles();
290
+ if (!newest || newest.port !== this.port || newest.token !== this.token) return null;
291
+ return newest;
292
+ }
293
+
253
294
  /** Check if any RPC server is reachable. */
254
295
  async isAvailable(): Promise<boolean> {
255
296
  try {
@@ -319,6 +319,10 @@ export class AftRpcServer {
319
319
  return json({ error: "Invalid JSON" }, 400);
320
320
  }
321
321
 
322
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
323
+ return json({ error: "Expected a JSON object" }, 400);
324
+ }
325
+
322
326
  if (!tokensMatch(typeof params.token === "string" ? params.token : "", this.token)) {
323
327
  return json({ error: "Forbidden" }, 403);
324
328
  }
@@ -453,6 +457,12 @@ export class AftRpcServer {
453
457
  return;
454
458
  }
455
459
 
460
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
461
+ res.writeHead(400, { "Content-Type": "application/json" });
462
+ res.end(JSON.stringify({ error: "Expected a JSON object" }));
463
+ return;
464
+ }
465
+
456
466
  if (!tokensMatch(typeof params.token === "string" ? params.token : "", this.token)) {
457
467
  res.writeHead(403, { "Content-Type": "application/json" });
458
468
  res.end(JSON.stringify({ error: "Forbidden" }));
@@ -68,12 +68,14 @@ function isV2PluginContext(client: unknown): client is V2PluginContextShape {
68
68
  }
69
69
 
70
70
  /**
71
- * How long an OpenCode 2 session lookup may run. The lookup is awaited before
72
- * the first tool call of a session is routed, so a host that never answers
73
- * must not hold that call; after this long the call proceeds with the
74
- * Location's directory instead.
71
+ * How long a session lookup may run on either host version. Tool routing
72
+ * awaits this lookup, so an unresponsive host must not hold the call forever.
73
+ * After this budget the call proceeds with its own directory instead.
75
74
  */
76
- export const V2_SESSION_LOOKUP_TIMEOUT_MS = 3_000;
75
+ export const LOOKUP_TIMEOUT_MS = 1_000;
76
+ export const V2_SESSION_LOOKUP_TIMEOUT_MS = LOOKUP_TIMEOUT_MS;
77
+ /** A failed lookup is provisional; retry later without querying on every call. */
78
+ export const FAILED_LOOKUP_RETRY_MS = 30_000;
77
79
 
78
80
  interface OpenCodeClientShape {
79
81
  session?: {
@@ -86,14 +88,15 @@ interface OpenCodeClientShape {
86
88
  }
87
89
 
88
90
  interface CacheEntry {
89
- /** Resolved directory, or `null` if lookup failed and we should not retry. */
91
+ /** Resolved directory, or `null` while a failed lookup is cooling down. */
90
92
  directory: string | null;
91
- /** Wall-clock timestamp of the cache entry, used only for LRU eviction. */
93
+ /** When the answer was recorded; failed lookups become retryable after 30 seconds. */
92
94
  recordedAt: number;
93
95
  }
94
96
 
95
97
  const CACHE_MAX_ENTRIES = 200;
96
98
  const cache = new Map<string, CacheEntry>();
99
+ const inflight = new Map<string, Promise<string | null>>();
97
100
 
98
101
  /**
99
102
  * Resolve the project directory the session was created with from the SDK's
@@ -114,19 +117,63 @@ export async function getSessionDirectory(
114
117
  if (!sessionId) return null;
115
118
 
116
119
  const cached = cache.get(sessionId);
117
- if (cached) {
120
+ if (
121
+ cached &&
122
+ (cached.directory !== null || Date.now() - cached.recordedAt < FAILED_LOOKUP_RETRY_MS)
123
+ ) {
118
124
  // Refresh LRU position
119
125
  cache.delete(sessionId);
120
126
  cache.set(sessionId, cached);
121
127
  return cached.directory;
122
128
  }
123
129
 
130
+ const pending = inflight.get(sessionId);
131
+ if (pending) return pending;
132
+ const lookup = boundedLookup(client, sessionId, fallbackDirectory).finally(() => {
133
+ inflight.delete(sessionId);
134
+ });
135
+ inflight.set(sessionId, lookup);
136
+ return lookup;
137
+ }
138
+
139
+ /** Ignore late answers: a timed-out lookup must not overwrite a later retry. */
140
+ async function boundedLookup(
141
+ client: unknown,
142
+ sessionId: string,
143
+ fallbackDirectory: string,
144
+ ): Promise<string | null> {
145
+ let timer: ReturnType<typeof setTimeout> | undefined;
146
+ try {
147
+ const timeout = new Promise<null>((resolve) => {
148
+ timer = setTimeout(() => {
149
+ sessionWarn(
150
+ sessionId,
151
+ `[aft-plugin] session lookup did not answer within ${LOOKUP_TIMEOUT_MS}ms; using the call's directory for now`,
152
+ );
153
+ resolve(null);
154
+ }, LOOKUP_TIMEOUT_MS);
155
+ });
156
+ const directory = await Promise.race([
157
+ lookupSessionDirectory(client, sessionId, fallbackDirectory),
158
+ timeout,
159
+ ]);
160
+ setCache(sessionId, directory);
161
+ return directory;
162
+ } finally {
163
+ clearTimeout(timer);
164
+ }
165
+ }
166
+
167
+ async function lookupSessionDirectory(
168
+ client: unknown,
169
+ sessionId: string,
170
+ fallbackDirectory: string,
171
+ ): Promise<string | null> {
124
172
  if (isV2PluginContext(client)) return lookupV2SessionDirectory(client, sessionId);
125
173
 
126
174
  const c = client as OpenCodeClientShape;
127
175
  const sessionApi = c?.session;
128
176
  if (!sessionApi || typeof sessionApi.get !== "function") {
129
- setCache(sessionId, null);
130
177
  return null;
131
178
  }
132
179
 
@@ -148,7 +195,7 @@ export async function getSessionDirectory(
148
195
  dir = session.directory;
149
196
  }
150
197
  } catch (err) {
151
- // Don't poison the cache on transient errors — but do log once.
198
+ // The bounded caller caches failures briefly so a struggling host is not polled per call.
152
199
  sessionWarn(
153
200
  sessionId,
154
201
  `[aft-plugin] session.get lookup failed: ${err instanceof Error ? err.message : String(err)}`,
@@ -156,7 +203,6 @@ export async function getSessionDirectory(
156
203
  return null;
157
204
  }
158
205
 
159
- setCache(sessionId, dir);
160
206
  return dir;
161
207
  }
162
208
 
@@ -164,12 +210,10 @@ export async function getSessionDirectory(
164
210
  * OpenCode 2 lookup: run the context's `session.get` Effect under a timeout and
165
211
  * read `location.directory`.
166
212
  *
167
- * Unlike the OpenCode 1 path, a failure or timeout is cached (as `null`), so it
168
- * is logged once per session and never retried. The fallback is already right:
169
- * with no cached directory, `projectRootFor` uses the tool call's Location
170
- * directory for OpenCode 2 runtimes, and OpenCode 2 runs every session inside
171
- * the Location created for that session's own directory. Retrying would only
172
- * add up to the timeout to every later tool call while the host misbehaves.
213
+ * The outer bounded lookup applies the same retry policy as OpenCode 1.
214
+ * The Effect timeout also interrupts the abandoned V2 lookup; fallback remains
215
+ * the tool call's runtime.directory (the host's Location directory), never
216
+ * the main checkout.
173
217
  */
174
218
  async function lookupV2SessionDirectory(
175
219
  context: V2PluginContextShape,
@@ -196,7 +240,6 @@ async function lookupV2SessionDirectory(
196
240
  `[aft-plugin] OpenCode 2 session lookup failed (${failure}); tool paths and bash use the Location directory ${context.location.directory} for this session`,
197
241
  );
198
242
  }
199
- setCache(sessionId, dir);
200
243
  return dir;
201
244
  }
202
245
 
@@ -226,6 +269,10 @@ export function getSessionDirectoryCached(
226
269
  if (!sessionId) return undefined;
227
270
  const cached = cache.get(sessionId);
228
271
  if (!cached) return undefined;
272
+ if (cached.directory === null && Date.now() - cached.recordedAt >= FAILED_LOOKUP_RETRY_MS) {
273
+ cache.delete(sessionId);
274
+ return undefined;
275
+ }
229
276
  return cached.directory;
230
277
  }
231
278
 
@@ -240,7 +287,7 @@ export function warmSessionDirectory(
240
287
  fallbackDirectory: string,
241
288
  ): void {
242
289
  if (!sessionId) return;
243
- if (cache.has(sessionId)) return;
290
+ if (getSessionDirectoryCached(sessionId) !== undefined) return;
244
291
  void getSessionDirectory(client, sessionId, fallbackDirectory);
245
292
  }
246
293
 
@@ -302,5 +349,6 @@ export async function verifySessionDirectory(
302
349
  /** Test-only: clear the cache between unit tests. */
303
350
  export function _resetSessionDirectoryCacheForTest(): void {
304
351
  cache.clear();
352
+ inflight.clear();
305
353
  verifyCache.clear();
306
354
  }
@@ -55,6 +55,7 @@ export interface StatusCompression {
55
55
  * absence as a clean zero.
56
56
  */
57
57
  export interface StatusBar {
58
+ disabled_categories?: string[];
58
59
  errors?: number;
59
60
  warnings?: number;
60
61
  dead_code?: number;
@@ -214,6 +215,9 @@ function readCompression(value: unknown): StatusCompression | undefined {
214
215
  function readStatusBar(value: unknown): StatusBar | undefined {
215
216
  if (typeof value !== "object" || value === null) return undefined;
216
217
  const bar = asRecord(value);
218
+ const disabled = Array.isArray(bar.disabled_categories)
219
+ ? bar.disabled_categories.filter((key): key is string => typeof key === "string")
220
+ : [];
217
221
  const errors = readOptionalNumber(bar.errors);
218
222
  const warnings = readOptionalNumber(bar.warnings);
219
223
  const deadCode = readOptionalNumber(bar.dead_code);
@@ -228,11 +232,13 @@ function readStatusBar(value: unknown): StatusBar | undefined {
228
232
  unusedExports === null &&
229
233
  duplicates === null &&
230
234
  todos === null &&
231
- !noLanguageServer
235
+ !noLanguageServer &&
236
+ disabled.length === 0
232
237
  ) {
233
238
  return undefined;
234
239
  }
235
240
  return {
241
+ ...(disabled.length > 0 ? { disabled_categories: disabled } : {}),
236
242
  ...(errors !== null ? { errors } : {}),
237
243
  ...(warnings !== null ? { warnings } : {}),
238
244
  ...(deadCode !== null ? { dead_code: deadCode } : {}),
@@ -437,8 +443,10 @@ export function formatStatusDialogMessage(status: AftStatusSnapshot): string {
437
443
  lines.push(`- storage dir: ${status.storage_dir ?? status.disk.storage_dir}`);
438
444
  }
439
445
 
440
- if (status.status_bar) {
441
- const sb = status.status_bar;
446
+ const sb = status.status_bar ?? status.status_bar_values;
447
+ if (sb) {
448
+ for (const key of sb.disabled_categories ?? [])
449
+ lines.push(`- ${key.replaceAll("_", " ")}: ○ off`);
442
450
  const rows = [
443
451
  ["errors", sb.errors],
444
452
  ["warnings", sb.warnings],
@@ -594,8 +602,10 @@ export function formatStatusMarkdown(status: AftStatusSnapshot): string {
594
602
  lines.push(`- **Storage dir:** \`${status.storage_dir ?? status.disk.storage_dir}\``);
595
603
  }
596
604
 
597
- if (status.status_bar) {
598
- const sb = status.status_bar;
605
+ const sb = status.status_bar ?? status.status_bar_values;
606
+ if (sb) {
607
+ for (const key of sb.disabled_categories ?? [])
608
+ lines.push(`- **${key.replaceAll("_", " ")}:** ○ off`);
599
609
  const rows = [
600
610
  ["Errors", sb.errors],
601
611
  ["Warnings", sb.warnings],
@@ -0,0 +1,7 @@
1
+ /** V2 passes a Location-scoped Effect context where V1 passes an SDK client. */
2
+ export function isV2PluginContext(client: unknown): boolean {
3
+ return (
4
+ typeof (client as { location?: { directory?: unknown } } | null)?.location?.directory ===
5
+ "string"
6
+ );
7
+ }