@draht/coding-agent 2026.3.6 → 2026.3.14

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 (177) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +6 -2
  3. package/bin/draht-tools.cjs +187 -32
  4. package/dist/cli/args.d.ts +6 -0
  5. package/dist/cli/args.d.ts.map +1 -1
  6. package/dist/cli/args.js +24 -0
  7. package/dist/cli/args.js.map +1 -1
  8. package/dist/cli/attach-mode.d.ts +13 -0
  9. package/dist/cli/attach-mode.d.ts.map +1 -0
  10. package/dist/cli/attach-mode.js +97 -0
  11. package/dist/cli/attach-mode.js.map +1 -0
  12. package/dist/cli/list-sessions.d.ts +8 -0
  13. package/dist/cli/list-sessions.d.ts.map +1 -0
  14. package/dist/cli/list-sessions.js +52 -0
  15. package/dist/cli/list-sessions.js.map +1 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/config.js +2 -2
  18. package/dist/config.js.map +1 -1
  19. package/dist/core/agent-session.d.ts +1 -0
  20. package/dist/core/agent-session.d.ts.map +1 -1
  21. package/dist/core/agent-session.js +50 -17
  22. package/dist/core/agent-session.js.map +1 -1
  23. package/dist/core/auth-storage.d.ts +2 -1
  24. package/dist/core/auth-storage.d.ts.map +1 -1
  25. package/dist/core/auth-storage.js +25 -1
  26. package/dist/core/auth-storage.js.map +1 -1
  27. package/dist/core/compaction/utils.d.ts +3 -0
  28. package/dist/core/compaction/utils.d.ts.map +1 -1
  29. package/dist/core/compaction/utils.js +16 -1
  30. package/dist/core/compaction/utils.js.map +1 -1
  31. package/dist/core/export-html/index.d.ts +5 -2
  32. package/dist/core/export-html/index.d.ts.map +1 -1
  33. package/dist/core/export-html/index.js +4 -3
  34. package/dist/core/export-html/index.js.map +1 -1
  35. package/dist/core/export-html/template.js +11 -14
  36. package/dist/core/export-html/tool-renderer.d.ts +5 -2
  37. package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
  38. package/dist/core/export-html/tool-renderer.js +12 -5
  39. package/dist/core/export-html/tool-renderer.js.map +1 -1
  40. package/dist/core/extensions/index.d.ts +1 -1
  41. package/dist/core/extensions/index.d.ts.map +1 -1
  42. package/dist/core/extensions/index.js.map +1 -1
  43. package/dist/core/extensions/loader.d.ts.map +1 -1
  44. package/dist/core/extensions/loader.js +6 -6
  45. package/dist/core/extensions/loader.js.map +1 -1
  46. package/dist/core/extensions/runner.d.ts +3 -2
  47. package/dist/core/extensions/runner.d.ts.map +1 -1
  48. package/dist/core/extensions/runner.js +32 -0
  49. package/dist/core/extensions/runner.js.map +1 -1
  50. package/dist/core/extensions/types.d.ts +21 -2
  51. package/dist/core/extensions/types.d.ts.map +1 -1
  52. package/dist/core/extensions/types.js.map +1 -1
  53. package/dist/core/model-resolver.d.ts.map +1 -1
  54. package/dist/core/model-resolver.js +2 -2
  55. package/dist/core/model-resolver.js.map +1 -1
  56. package/dist/core/package-manager.d.ts.map +1 -1
  57. package/dist/core/package-manager.js +2 -2
  58. package/dist/core/package-manager.js.map +1 -1
  59. package/dist/core/resource-loader.d.ts.map +1 -1
  60. package/dist/core/resource-loader.js +1 -1
  61. package/dist/core/resource-loader.js.map +1 -1
  62. package/dist/core/sdk.d.ts.map +1 -1
  63. package/dist/core/sdk.js +7 -0
  64. package/dist/core/sdk.js.map +1 -1
  65. package/dist/core/settings-manager.d.ts +4 -0
  66. package/dist/core/settings-manager.d.ts.map +1 -1
  67. package/dist/core/settings-manager.js +36 -2
  68. package/dist/core/settings-manager.js.map +1 -1
  69. package/dist/core/socket-server/discovery.d.ts +19 -0
  70. package/dist/core/socket-server/discovery.d.ts.map +1 -0
  71. package/dist/core/socket-server/discovery.js +91 -0
  72. package/dist/core/socket-server/discovery.js.map +1 -0
  73. package/dist/core/socket-server/index.d.ts +13 -0
  74. package/dist/core/socket-server/index.d.ts.map +1 -0
  75. package/dist/core/socket-server/index.js +11 -0
  76. package/dist/core/socket-server/index.js.map +1 -0
  77. package/dist/core/socket-server/session-integration.d.ts +17 -0
  78. package/dist/core/socket-server/session-integration.d.ts.map +1 -0
  79. package/dist/core/socket-server/session-integration.js +77 -0
  80. package/dist/core/socket-server/session-integration.js.map +1 -0
  81. package/dist/core/socket-server/socket-client.d.ts +65 -0
  82. package/dist/core/socket-server/socket-client.d.ts.map +1 -0
  83. package/dist/core/socket-server/socket-client.js +197 -0
  84. package/dist/core/socket-server/socket-client.js.map +1 -0
  85. package/dist/core/socket-server/socket-server.d.ts +60 -0
  86. package/dist/core/socket-server/socket-server.d.ts.map +1 -0
  87. package/dist/core/socket-server/socket-server.js +273 -0
  88. package/dist/core/socket-server/socket-server.js.map +1 -0
  89. package/dist/core/socket-server/types.d.ts +81 -0
  90. package/dist/core/socket-server/types.d.ts.map +1 -0
  91. package/dist/core/socket-server/types.js +8 -0
  92. package/dist/core/socket-server/types.js.map +1 -0
  93. package/dist/gsd/domain.d.ts +5 -1
  94. package/dist/gsd/domain.d.ts.map +1 -1
  95. package/dist/gsd/domain.js +71 -1
  96. package/dist/gsd/domain.js.map +1 -1
  97. package/dist/gsd/git.d.ts.map +1 -1
  98. package/dist/gsd/git.js +18 -0
  99. package/dist/gsd/git.js.map +1 -1
  100. package/dist/gsd/index.d.ts +1 -0
  101. package/dist/gsd/index.d.ts.map +1 -1
  102. package/dist/gsd/index.js.map +1 -1
  103. package/dist/index.d.ts +1 -1
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.js.map +1 -1
  106. package/dist/main.d.ts.map +1 -1
  107. package/dist/main.js +76 -11
  108. package/dist/main.js.map +1 -1
  109. package/dist/modes/interactive/components/extension-editor.d.ts.map +1 -1
  110. package/dist/modes/interactive/components/extension-editor.js +1 -0
  111. package/dist/modes/interactive/components/extension-editor.js.map +1 -1
  112. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  113. package/dist/modes/interactive/components/footer.js +8 -23
  114. package/dist/modes/interactive/components/footer.js.map +1 -1
  115. package/dist/modes/interactive/components/settings-selector.d.ts +2 -0
  116. package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/settings-selector.js +10 -0
  118. package/dist/modes/interactive/components/settings-selector.js.map +1 -1
  119. package/dist/modes/interactive/components/tool-execution.d.ts.map +1 -1
  120. package/dist/modes/interactive/components/tool-execution.js +14 -4
  121. package/dist/modes/interactive/components/tool-execution.js.map +1 -1
  122. package/dist/modes/interactive/components/tree-selector.d.ts +21 -2
  123. package/dist/modes/interactive/components/tree-selector.d.ts.map +1 -1
  124. package/dist/modes/interactive/components/tree-selector.js +115 -9
  125. package/dist/modes/interactive/components/tree-selector.js.map +1 -1
  126. package/dist/modes/interactive/interactive-mode.d.ts +1 -0
  127. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  128. package/dist/modes/interactive/interactive-mode.js +66 -5
  129. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  130. package/dist/modes/rpc/jsonl.d.ts +17 -0
  131. package/dist/modes/rpc/jsonl.d.ts.map +1 -0
  132. package/dist/modes/rpc/jsonl.js +49 -0
  133. package/dist/modes/rpc/jsonl.js.map +1 -0
  134. package/dist/modes/rpc/rpc-client.d.ts +1 -1
  135. package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
  136. package/dist/modes/rpc/rpc-client.js +7 -11
  137. package/dist/modes/rpc/rpc-client.js.map +1 -1
  138. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  139. package/dist/modes/rpc/rpc-mode.js +9 -11
  140. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  141. package/dist/prompts/commands/execute-phase.md +2 -2
  142. package/dist/prompts/commands/fix.md +2 -2
  143. package/dist/prompts/commands/plan-phase.md +5 -1
  144. package/dist/prompts/commands/quick.md +5 -1
  145. package/dist/utils/changelog.d.ts +12 -0
  146. package/dist/utils/changelog.d.ts.map +1 -1
  147. package/dist/utils/changelog.js +25 -14
  148. package/dist/utils/changelog.js.map +1 -1
  149. package/dist/utils/notify.d.ts +12 -0
  150. package/dist/utils/notify.d.ts.map +1 -0
  151. package/dist/utils/notify.js +41 -0
  152. package/dist/utils/notify.js.map +1 -0
  153. package/docs/compaction.md +2 -0
  154. package/docs/custom-provider.md +11 -7
  155. package/docs/extensions.md +55 -3
  156. package/docs/keybindings.md +9 -1
  157. package/docs/models.md +5 -1
  158. package/docs/rpc.md +40 -3
  159. package/docs/session.md +2 -2
  160. package/docs/settings.md +1 -0
  161. package/docs/terminal-setup.md +28 -3
  162. package/docs/tmux.md +61 -0
  163. package/docs/tree.md +9 -0
  164. package/examples/extensions/antigravity-image-gen.ts +5 -4
  165. package/examples/extensions/custom-provider-gitlab-duo/test.ts +2 -2
  166. package/examples/extensions/notify.ts +9 -2
  167. package/examples/extensions/overlay-qa-tests.ts +468 -1
  168. package/examples/extensions/preset.ts +2 -3
  169. package/examples/extensions/provider-payload.ts +14 -0
  170. package/examples/extensions/sandbox/index.ts +2 -3
  171. package/examples/extensions/tool-override.ts +2 -3
  172. package/examples/extensions/with-deps/index.ts +1 -5
  173. package/package.json +7 -5
  174. package/prompts/commands/execute-phase.md +2 -2
  175. package/prompts/commands/fix.md +2 -2
  176. package/prompts/commands/plan-phase.md +5 -1
  177. package/prompts/commands/quick.md +5 -1
@@ -14,10 +14,13 @@
14
14
  * /overlay-maxheight - Test maxHeight truncation
15
15
  * /overlay-sidepanel - Responsive sidepanel (hides when terminal < 100 cols)
16
16
  * /overlay-toggle - Toggle visibility demo (demonstrates OverlayHandle.setHidden)
17
+ * /overlay-passive - Non-capturing overlay demo (passive info panel alongside active overlay)
18
+ * /overlay-focus - Focus cycling and rendering order with non-capturing overlays
19
+ * /overlay-streaming - Multiple input panels with simulated streaming (Tab to cycle focus)
17
20
  */
18
21
 
19
22
  import type { ExtensionAPI, ExtensionCommandContext, Theme } from "@draht/coding-agent";
20
- import type { OverlayAnchor, OverlayHandle, OverlayOptions, TUI } from "@draht/tui";
23
+ import type { Component, OverlayAnchor, OverlayHandle, OverlayOptions, TUI } from "@draht/tui";
21
24
  import { matchesKey, truncateToWidth, visibleWidth } from "@draht/tui";
22
25
  import { spawn } from "child_process";
23
26
 
@@ -256,6 +259,42 @@ export default function (pi: ExtensionAPI) {
256
259
  globalToggleHandle = null;
257
260
  },
258
261
  });
262
+
263
+ // Non-capturing overlay demo - passive info panel that doesn't steal focus
264
+ pi.registerCommand("overlay-passive", {
265
+ description: "Test non-capturing overlay (passive info panel alongside active overlay)",
266
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
267
+ ctx.ui.setEditorText("");
268
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => new PassiveDemoController(tui, theme, done), {
269
+ overlay: true,
270
+ overlayOptions: { anchor: "center", width: 48 },
271
+ });
272
+ },
273
+ });
274
+
275
+ // Focus cycling demo - demonstrates focus(), unfocus(), isFocused() and rendering order
276
+ pi.registerCommand("overlay-focus", {
277
+ description: "Test focus cycling and rendering order with non-capturing overlays",
278
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
279
+ ctx.ui.setEditorText("");
280
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => new FocusDemoController(tui, theme, done), {
281
+ overlay: true,
282
+ overlayOptions: { anchor: "bottom-center", width: 55, margin: { bottom: 1 } },
283
+ });
284
+ },
285
+ });
286
+
287
+ // Test multiple input panels with simulated streaming
288
+ pi.registerCommand("overlay-streaming", {
289
+ description: "Multiple input panels with simulated streaming (Tab to cycle focus)",
290
+ handler: async (_args: string, ctx: ExtensionCommandContext) => {
291
+ ctx.ui.setEditorText("");
292
+ await ctx.ui.custom<void>((tui, theme, _kb, done) => new StreamingInputController(tui, theme, done), {
293
+ overlay: true,
294
+ overlayOptions: { anchor: "bottom-center", width: 60, margin: { bottom: 1 } },
295
+ });
296
+ },
297
+ });
259
298
  }
260
299
 
261
300
  function sleep(ms: number): Promise<void> {
@@ -879,3 +918,431 @@ class ToggleDemoComponent extends BaseOverlay {
879
918
  );
880
919
  }
881
920
  }
921
+
922
+ // === Non-capturing passive overlay demo ===
923
+
924
+ class PassiveDemoController extends BaseOverlay {
925
+ focused = false;
926
+ private typed = "";
927
+ private timerComponent: TimerPanel;
928
+ private timerHandle: OverlayHandle | null = null;
929
+ private interval: ReturnType<typeof setInterval> | null = null;
930
+ private inputCount = 0;
931
+ private lastInputDebug = "";
932
+
933
+ constructor(
934
+ private tui: TUI,
935
+ theme: Theme,
936
+ private done: () => void,
937
+ ) {
938
+ super(theme);
939
+ this.timerComponent = new TimerPanel(theme);
940
+ this.timerHandle = this.tui.showOverlay(this.timerComponent, {
941
+ nonCapturing: true,
942
+ anchor: "top-right",
943
+ width: 22,
944
+ margin: { top: 1, right: 2 },
945
+ });
946
+ this.interval = setInterval(() => {
947
+ this.timerComponent.tick();
948
+ this.tui.requestRender();
949
+ }, 1000);
950
+ }
951
+
952
+ handleInput(data: string): void {
953
+ this.inputCount++;
954
+ this.lastInputDebug = `len=${data.length} c0=${data.charCodeAt(0)}`;
955
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
956
+ this.cleanup();
957
+ this.done();
958
+ } else if (matchesKey(data, "backspace")) {
959
+ this.typed = this.typed.slice(0, -1);
960
+ } else if (data.length === 1 && data.charCodeAt(0) >= 32) {
961
+ this.typed += data;
962
+ }
963
+ }
964
+
965
+ render(width: number): string[] {
966
+ const th = this.theme;
967
+ const display = this.typed.length > 0 ? this.typed : th.fg("dim", "(type here)");
968
+ return this.box(
969
+ [
970
+ "",
971
+ ` ${th.fg("dim", `focused=${this.focused} inputs=${this.inputCount}`)}`,
972
+ ` ${th.fg("dim", `last: ${this.lastInputDebug || "none"}`)}`,
973
+ "",
974
+ ` > ${display}`,
975
+ "",
976
+ th.fg("dim", " Type to prove input goes here."),
977
+ th.fg("dim", " Press Esc to close both."),
978
+ "",
979
+ ],
980
+ width,
981
+ "Non-Capturing Demo",
982
+ );
983
+ }
984
+
985
+ private cleanup(): void {
986
+ if (this.interval) {
987
+ clearInterval(this.interval);
988
+ this.interval = null;
989
+ }
990
+ this.timerHandle?.hide();
991
+ this.timerHandle = null;
992
+ }
993
+
994
+ override dispose(): void {
995
+ this.cleanup();
996
+ }
997
+ }
998
+
999
+ class TimerPanel extends BaseOverlay {
1000
+ private seconds = 0;
1001
+
1002
+ tick(): void {
1003
+ this.seconds++;
1004
+ }
1005
+
1006
+ render(width: number): string[] {
1007
+ const th = this.theme;
1008
+ const mins = Math.floor(this.seconds / 60);
1009
+ const secs = this.seconds % 60;
1010
+ const time = `${String(mins).padStart(2, "0")}:${String(secs).padStart(2, "0")}`;
1011
+ return this.box([` ${th.fg("accent", time)}`, th.fg("dim", " nonCapturing: true")], width, "Timer");
1012
+ }
1013
+ }
1014
+
1015
+ // === Focus cycling demo ===
1016
+
1017
+ class FocusDemoController extends BaseOverlay {
1018
+ private panels: FocusPanel[] = [];
1019
+ private handles: OverlayHandle[] = [];
1020
+ private focusIndex = -1;
1021
+
1022
+ constructor(
1023
+ private tui: TUI,
1024
+ theme: Theme,
1025
+ private done: () => void,
1026
+ ) {
1027
+ super(theme);
1028
+ const colors = ["error", "success", "accent"] as const;
1029
+ const labels = ["Alpha", "Beta", "Gamma"];
1030
+
1031
+ for (let i = 0; i < 3; i++) {
1032
+ const panel = new FocusPanel(
1033
+ theme,
1034
+ labels[i]!,
1035
+ colors[i]!,
1036
+ () => this.cycleFocus(),
1037
+ () => this.close(),
1038
+ );
1039
+ const handle = this.tui.showOverlay(panel, {
1040
+ nonCapturing: true,
1041
+ row: 2,
1042
+ col: 5 + i * 6,
1043
+ width: 28,
1044
+ });
1045
+ panel.handle = handle;
1046
+ this.panels.push(panel);
1047
+ this.handles.push(handle);
1048
+ }
1049
+ }
1050
+
1051
+ private cycleFocus(): void {
1052
+ if (this.focusIndex >= 0 && this.focusIndex < this.handles.length) {
1053
+ this.handles[this.focusIndex]!.unfocus();
1054
+ }
1055
+ this.focusIndex++;
1056
+ if (this.focusIndex >= this.handles.length) {
1057
+ this.focusIndex = -1;
1058
+ } else {
1059
+ this.handles[this.focusIndex]!.focus();
1060
+ }
1061
+ this.tui.requestRender();
1062
+ }
1063
+
1064
+ private close(): void {
1065
+ for (const handle of this.handles) handle.hide();
1066
+ this.handles = [];
1067
+ this.panels = [];
1068
+ this.done();
1069
+ }
1070
+
1071
+ handleInput(data: string): void {
1072
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
1073
+ this.close();
1074
+ } else if (matchesKey(data, "tab")) {
1075
+ this.cycleFocus();
1076
+ }
1077
+ }
1078
+
1079
+ render(width: number): string[] {
1080
+ const th = this.theme;
1081
+ const focused = this.focusIndex === -1 ? "Controller" : (this.panels[this.focusIndex]?.label ?? "?");
1082
+ return this.box(
1083
+ [
1084
+ "",
1085
+ ` Current focus: ${th.fg("accent", focused)}`,
1086
+ "",
1087
+ " Three overlapping panels above are",
1088
+ ` all ${th.fg("accent", "nonCapturing")}. Press Tab to`,
1089
+ " cycle focus() between them.",
1090
+ "",
1091
+ " Focused panel renders on top",
1092
+ " (focus-based rendering order).",
1093
+ "",
1094
+ th.fg("dim", " Tab = cycle focus | Esc = close"),
1095
+ "",
1096
+ ],
1097
+ width,
1098
+ "Focus Demo",
1099
+ );
1100
+ }
1101
+
1102
+ override dispose(): void {
1103
+ for (const handle of this.handles) handle.hide();
1104
+ }
1105
+ }
1106
+
1107
+ class FocusPanel extends BaseOverlay {
1108
+ handle: OverlayHandle | null = null;
1109
+ readonly label: string;
1110
+
1111
+ constructor(
1112
+ theme: Theme,
1113
+ label: string,
1114
+ private color: "error" | "success" | "accent",
1115
+ private onTab: () => void,
1116
+ private onClose: () => void,
1117
+ ) {
1118
+ super(theme);
1119
+ this.label = label;
1120
+ }
1121
+
1122
+ handleInput(data: string): void {
1123
+ if (matchesKey(data, "tab")) {
1124
+ this.onTab();
1125
+ } else if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
1126
+ this.onClose();
1127
+ }
1128
+ }
1129
+
1130
+ render(width: number): string[] {
1131
+ const th = this.theme;
1132
+ const focused = this.handle?.isFocused() ?? false;
1133
+ const innerW = Math.max(1, width - 2);
1134
+ const border = (c: string) => th.fg(this.color, c);
1135
+ const padLine = (s: string) => truncateToWidth(s, innerW, "...", true);
1136
+ const lines: string[] = [];
1137
+
1138
+ lines.push(border(`╭${"─".repeat(innerW)}╮`));
1139
+ lines.push(border("│") + padLine(` ${th.fg("accent", this.label)}`) + border("│"));
1140
+ lines.push(border("│") + padLine("") + border("│"));
1141
+ if (focused) {
1142
+ lines.push(border("│") + padLine(th.fg("success", " ● FOCUSED")) + border("│"));
1143
+ lines.push(border("│") + padLine(th.fg("dim", " (receiving input)")) + border("│"));
1144
+ } else {
1145
+ lines.push(border("│") + padLine(th.fg("dim", " ○ unfocused")) + border("│"));
1146
+ lines.push(border("│") + padLine(th.fg("dim", " (passive)")) + border("│"));
1147
+ }
1148
+ lines.push(border("│") + padLine("") + border("│"));
1149
+ lines.push(border(`╰${"─".repeat(innerW)}╯`));
1150
+
1151
+ return lines;
1152
+ }
1153
+ }
1154
+
1155
+ // === Streaming input panel test (/overlay-streaming) ===
1156
+
1157
+ class StreamingInputController extends BaseOverlay {
1158
+ private panels: StreamingInputPanel[] = [];
1159
+ private handles: OverlayHandle[] = [];
1160
+ private focusIndex = -1; // -1 = controller focused, 0-2 = panel focused
1161
+ private streamLines: string[] = [];
1162
+ private streamInterval: ReturnType<typeof setInterval> | null = null;
1163
+ private lineCount = 0;
1164
+
1165
+ constructor(
1166
+ private tui: TUI,
1167
+ theme: Theme,
1168
+ private done: () => void,
1169
+ ) {
1170
+ super(theme);
1171
+
1172
+ // Create 3 input panels as non-capturing overlays
1173
+ const colors = ["error", "success", "accent"] as const;
1174
+ const labels = ["Panel A", "Panel B", "Panel C"];
1175
+
1176
+ for (let i = 0; i < 3; i++) {
1177
+ const panel = new StreamingInputPanel(
1178
+ theme,
1179
+ labels[i]!,
1180
+ colors[i]!,
1181
+ () => this.cycleFocus(),
1182
+ () => this.close(),
1183
+ );
1184
+ const handle = this.tui.showOverlay(panel, {
1185
+ nonCapturing: true,
1186
+ row: 1 + i * 9,
1187
+ col: 2,
1188
+ width: 35,
1189
+ });
1190
+ panel.handle = handle;
1191
+ this.panels.push(panel);
1192
+ this.handles.push(handle);
1193
+ }
1194
+
1195
+ // Start with controller focused (focusIndex = -1)
1196
+
1197
+ // Start simulated streaming
1198
+ this.streamInterval = setInterval(() => {
1199
+ this.lineCount++;
1200
+ const timestamp = new Date().toLocaleTimeString();
1201
+ this.streamLines.push(`[${timestamp}] Streaming line ${this.lineCount}...`);
1202
+ if (this.streamLines.length > 8) {
1203
+ this.streamLines.shift();
1204
+ }
1205
+ this.tui.requestRender();
1206
+ }, 500);
1207
+ }
1208
+
1209
+ private cycleFocus(): void {
1210
+ // Unfocus current panel if any
1211
+ if (this.focusIndex >= 0 && this.focusIndex < this.handles.length) {
1212
+ this.handles[this.focusIndex]!.unfocus();
1213
+ }
1214
+
1215
+ // Cycle: -1 (controller) → 0 → 1 → 2 → -1 ...
1216
+ this.focusIndex++;
1217
+ if (this.focusIndex >= this.handles.length) {
1218
+ this.focusIndex = -1; // Back to controller
1219
+ }
1220
+
1221
+ // Focus new panel if any
1222
+ if (this.focusIndex >= 0) {
1223
+ this.handles[this.focusIndex]!.focus();
1224
+ }
1225
+
1226
+ this.tui.requestRender();
1227
+ }
1228
+
1229
+ private close(): void {
1230
+ if (this.streamInterval) {
1231
+ clearInterval(this.streamInterval);
1232
+ this.streamInterval = null;
1233
+ }
1234
+ for (const handle of this.handles) handle.hide();
1235
+ this.handles = [];
1236
+ this.panels = [];
1237
+ this.done();
1238
+ }
1239
+
1240
+ handleInput(data: string): void {
1241
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
1242
+ this.close();
1243
+ } else if (matchesKey(data, "tab")) {
1244
+ this.cycleFocus();
1245
+ }
1246
+ }
1247
+
1248
+ render(width: number): string[] {
1249
+ const th = this.theme;
1250
+ const focusedLabel =
1251
+ this.focusIndex === -1
1252
+ ? th.fg("success", "Controller (this panel)")
1253
+ : (this.panels[this.focusIndex]?.label ?? "?");
1254
+
1255
+ const lines = [
1256
+ "",
1257
+ ` Current focus: ${th.fg("accent", focusedLabel)}`,
1258
+ "",
1259
+ " Simulated streaming output:",
1260
+ th.fg("dim", " ─".repeat((width - 2) / 2)),
1261
+ ];
1262
+
1263
+ for (const line of this.streamLines) {
1264
+ lines.push(` ${th.fg("dim", line)}`);
1265
+ }
1266
+
1267
+ while (lines.length < 12) {
1268
+ lines.push("");
1269
+ }
1270
+
1271
+ lines.push(th.fg("dim", " ─".repeat((width - 2) / 2)));
1272
+ lines.push("");
1273
+ lines.push(` Three ${th.fg("accent", "nonCapturing")} input panels on the left.`);
1274
+ lines.push(" Tab cycles: Controller → Panel A → B → C → Controller");
1275
+ lines.push(" Type in each panel to test input routing.");
1276
+ lines.push("");
1277
+ lines.push(th.fg("dim", " Tab = cycle focus | Esc = close all"));
1278
+ lines.push("");
1279
+
1280
+ return this.box(lines, width, "Streaming + Input Test");
1281
+ }
1282
+
1283
+ override dispose(): void {
1284
+ this.close();
1285
+ }
1286
+ }
1287
+
1288
+ class StreamingInputPanel implements Component {
1289
+ handle: OverlayHandle | null = null;
1290
+ private typed = "";
1291
+ readonly label: string;
1292
+
1293
+ constructor(
1294
+ private theme: Theme,
1295
+ label: string,
1296
+ private color: "error" | "success" | "accent",
1297
+ private onTab: () => void,
1298
+ private onClose: () => void,
1299
+ ) {
1300
+ this.label = label;
1301
+ }
1302
+
1303
+ handleInput(data: string): void {
1304
+ if (matchesKey(data, "tab")) {
1305
+ this.onTab();
1306
+ } else if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) {
1307
+ this.onClose();
1308
+ } else if (matchesKey(data, "backspace")) {
1309
+ this.typed = this.typed.slice(0, -1);
1310
+ } else if (data.length === 1 && data.charCodeAt(0) >= 32) {
1311
+ this.typed += data;
1312
+ }
1313
+ }
1314
+
1315
+ render(width: number): string[] {
1316
+ const th = this.theme;
1317
+ const focused = this.handle?.isFocused() ?? false;
1318
+ const innerW = Math.max(1, width - 2);
1319
+ const border = (c: string) => th.fg(this.color, c);
1320
+ const padLine = (s: string) => {
1321
+ const w = visibleWidth(s);
1322
+ return s + " ".repeat(Math.max(0, innerW - w));
1323
+ };
1324
+
1325
+ const inputDisplay = this.typed.length > 0 ? this.typed : th.fg("dim", "(type here)");
1326
+ const truncatedInput = truncateToWidth(` > ${inputDisplay}`, innerW, "...", true);
1327
+
1328
+ const lines: string[] = [];
1329
+ lines.push(border(`╭${"─".repeat(innerW)}╮`));
1330
+ lines.push(border("│") + padLine(` ${th.fg("accent", this.label)}`) + border("│"));
1331
+ lines.push(border("│") + padLine("") + border("│"));
1332
+ if (focused) {
1333
+ lines.push(border("│") + padLine(th.fg("success", " ● FOCUSED")) + border("│"));
1334
+ lines.push(border("│") + padLine(th.fg("dim", " (receiving input)")) + border("│"));
1335
+ } else {
1336
+ lines.push(border("│") + padLine(th.fg("dim", " ○ unfocused")) + border("│"));
1337
+ lines.push(border("│") + padLine("") + border("│"));
1338
+ }
1339
+ lines.push(border("│") + padLine(truncatedInput) + border("│"));
1340
+ lines.push(border("│") + padLine("") + border("│"));
1341
+ lines.push(border("│") + padLine(th.fg("dim", " Tab | Esc")) + border("│"));
1342
+ lines.push(border(`╰${"─".repeat(innerW)}╯`));
1343
+
1344
+ return lines;
1345
+ }
1346
+
1347
+ invalidate(): void {}
1348
+ }
@@ -39,10 +39,9 @@
39
39
  */
40
40
 
41
41
  import { existsSync, readFileSync } from "node:fs";
42
- import { homedir } from "node:os";
43
42
  import { join } from "node:path";
44
43
  import type { ExtensionAPI, ExtensionContext } from "@draht/coding-agent";
45
- import { DynamicBorder } from "@draht/coding-agent";
44
+ import { DynamicBorder, getAgentDir } from "@draht/coding-agent";
46
45
  import { Container, Key, type SelectItem, SelectList, Text } from "@draht/tui";
47
46
 
48
47
  // Preset configuration
@@ -68,7 +67,7 @@ interface PresetsConfig {
68
67
  * Project-local presets override global presets with the same name.
69
68
  */
70
69
  function loadPresets(cwd: string): PresetsConfig {
71
- const globalPath = join(homedir(), ".pi", "agent", "presets.json");
70
+ const globalPath = join(getAgentDir(), "presets.json");
72
71
  const projectPath = join(cwd, ".pi", "presets.json");
73
72
 
74
73
  let globalPresets: PresetsConfig = {};
@@ -0,0 +1,14 @@
1
+ import { appendFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import type { ExtensionAPI } from "@draht/coding-agent";
4
+
5
+ export default function (pi: ExtensionAPI) {
6
+ const logFile = join(process.cwd(), ".pi", "provider-payload.log");
7
+
8
+ pi.on("before_provider_request", (event) => {
9
+ appendFileSync(logFile, `${JSON.stringify(event.payload, null, 2)}\n\n`, "utf8");
10
+
11
+ // Optional: replace the payload instead of only logging it.
12
+ // return { ...event.payload, temperature: 0 };
13
+ });
14
+ }
@@ -39,11 +39,10 @@
39
39
 
40
40
  import { spawn } from "node:child_process";
41
41
  import { existsSync, readFileSync } from "node:fs";
42
- import { homedir } from "node:os";
43
42
  import { join } from "node:path";
44
43
  import { SandboxManager, type SandboxRuntimeConfig } from "@anthropic-ai/sandbox-runtime";
45
44
  import type { ExtensionAPI } from "@draht/coding-agent";
46
- import { type BashOperations, createBashTool } from "@draht/coding-agent";
45
+ import { type BashOperations, createBashTool, getAgentDir } from "@draht/coding-agent";
47
46
 
48
47
  interface SandboxConfig extends SandboxRuntimeConfig {
49
48
  enabled?: boolean;
@@ -75,7 +74,7 @@ const DEFAULT_CONFIG: SandboxConfig = {
75
74
 
76
75
  function loadConfig(cwd: string): SandboxConfig {
77
76
  const projectConfigPath = join(cwd, ".pi", "sandbox.json");
78
- const globalConfigPath = join(homedir(), ".pi", "agent", "sandbox.json");
77
+ const globalConfigPath = join(getAgentDir(), "extensions", "sandbox.json");
79
78
 
80
79
  let globalConfig: Partial<SandboxConfig> = {};
81
80
  let projectConfig: Partial<SandboxConfig> = {};
@@ -21,14 +21,13 @@
21
21
  */
22
22
 
23
23
  import type { TextContent } from "@draht/ai";
24
- import type { ExtensionAPI } from "@draht/coding-agent";
24
+ import { type ExtensionAPI, getAgentDir } from "@draht/coding-agent";
25
25
  import { Type } from "@sinclair/typebox";
26
26
  import { appendFileSync, constants, readFileSync } from "fs";
27
27
  import { access, readFile } from "fs/promises";
28
- import { homedir } from "os";
29
28
  import { join, resolve } from "path";
30
29
 
31
- const LOG_FILE = join(homedir(), ".pi", "agent", "read-access.log");
30
+ const LOG_FILE = join(getAgentDir(), "read-access.log");
32
31
 
33
32
  // Paths that are blocked from reading
34
33
  const BLOCKED_PATTERNS = [
@@ -21,11 +21,7 @@ export default function (pi: ExtensionAPI) {
21
21
  execute: async (_toolCallId, params) => {
22
22
  const result = ms(params.duration as ms.StringValue);
23
23
  if (result === undefined) {
24
- return {
25
- content: [{ type: "text", text: `Invalid duration: "${params.duration}"` }],
26
- isError: true,
27
- details: {},
28
- };
24
+ throw new Error(`Invalid duration: "${params.duration}"`);
29
25
  }
30
26
  return {
31
27
  content: [{ type: "text", text: `${params.duration} = ${result} milliseconds` }],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@draht/coding-agent",
3
- "version": "2026.3.6",
3
+ "version": "2026.3.14",
4
4
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
5
5
  "type": "module",
6
6
  "drahtConfig": {
@@ -46,9 +46,11 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "@mariozechner/jiti": "^2.6.2",
49
- "@draht/agent-core": "2026.3.6",
50
- "@draht/ai": "2026.3.6",
51
- "@draht/tui": "2026.3.6",
49
+ "@sinclair/typebox": "^0.34.41",
50
+ "ajv": "^8.17.1",
51
+ "@draht/agent-core": "2026.3.14",
52
+ "@draht/ai": "2026.3.14",
53
+ "@draht/tui": "2026.3.14",
52
54
  "@silvia-odwyer/photon-node": "^0.3.4",
53
55
  "chalk": "^5.5.0",
54
56
  "cli-highlight": "^2.1.11",
@@ -103,6 +105,6 @@
103
105
  "directory": "packages/coding-agent"
104
106
  },
105
107
  "engines": {
106
- "node": ">=20.0.0"
108
+ "node": ">=20.6.0"
107
109
  }
108
110
  }
@@ -41,8 +41,8 @@ Execute this plan. Here is the full plan content:
41
41
  <paste full plan XML here>
42
42
 
43
43
  For each <task> in the plan, follow this TDD cycle:
44
- 1. RED — Write failing tests from <test>. Run the test runner, confirm they FAIL. Commit with: git add <test-files> && git commit -m "red: <description>"
45
- 2. GREEN — Write minimal implementation from <action> to make tests pass. Run tests, confirm PASS. Commit with: git add <files> && git commit -m "green: <task name>"
44
+ 1. RED — Write failing tests from <test>. Run the test runner, confirm they FAIL. Commit with: git add <test-files> && git commit -m "test: <description>"
45
+ 2. GREEN — Write minimal implementation from <action> to make tests pass. Run tests, confirm PASS. Commit with: git add <files> && git commit -m "feat: <task name>"
46
46
  3. REFACTOR — Apply <refactor> improvements if any. Tests must stay green after each change. Commit with: git add <files> && git commit -m "refactor: <description>"
47
47
  4. VERIFY — Run the <verify> step, confirm <done> criteria are met.
48
48
 
@@ -18,12 +18,12 @@ Issue: $ARGUMENTS
18
18
  "Diagnose this issue: $ARGUMENTS. Reproduce the bug by running the relevant test or command. Trace the root cause by reading the code. Identify the exact files and lines involved. Do NOT fix it yet — only report the diagnosis with: root cause, affected files, and a recommended fix approach. Do NOT run draht, draht-tools, or pi commands."
19
19
 
20
20
  2. **Write a reproducing test**: Based on the diagnosis, write a test that demonstrates the bug (it must fail)
21
- - Commit: `draht-tools commit-docs "red: reproduce bug"`
21
+ - Commit: `draht-tools commit-docs "test: reproduce bug"`
22
22
 
23
23
  3. **Minimal fix**: Write the smallest change that makes the test pass
24
24
  - Do not refactor or add features — just fix the bug
25
25
  - Run the full test suite to check for regressions
26
- - Commit: `draht-tools commit-docs "green: fix description"`
26
+ - Commit: `draht-tools commit-docs "fix: fix description"`
27
27
 
28
28
  4. **Refactor** (if needed): Clean up without changing behavior
29
29
  - Tests must stay green after every change
@@ -34,7 +34,11 @@ Phase: $1
34
34
  - Instruction to output the plan as XML (you will save it via `draht-tools create-plan`)
35
35
 
36
36
  6. Collect all plan outputs from subagents
37
- 7. Save plans yourself: `draht-tools create-plan $1 P` for each plan
37
+ 7. Save each plan by piping the subagent's output into `draht-tools create-plan`:
38
+ ```
39
+ echo 'plan content from subagent' | draht-tools create-plan $1 P [title]
40
+ ```
41
+ The content must contain real task details (files, actions, tests) — NOT placeholder brackets. If `create-plan` is called without stdin, it writes a useless template.
38
42
  8. Validate: `draht-tools validate-plans $1`
39
43
  9. Commit: `draht-tools commit-docs "create phase $1 plans"`
40
44
 
@@ -15,7 +15,11 @@ Task: $ARGUMENTS
15
15
 
16
16
  ## Steps
17
17
  1. Run `draht-tools next-quick-number` to get task number
18
- 2. Create quick plan: `draht-tools create-quick-plan NNN "$ARGUMENTS"`
18
+ 2. Analyze the task and write a concrete plan with actual task details (files, actions, verification). Pipe it into `draht-tools create-quick-plan`:
19
+ ```
20
+ echo 'plan content here' | draht-tools create-quick-plan NNN "$ARGUMENTS"
21
+ ```
22
+ The plan content must include: a `# Quick Task NNN: title` heading, a `## Tasks` section with one or more `<task>` XML blocks containing real file paths, real actions, and real verification steps — NOT placeholders like `[files]`.
19
23
  3. **Delegate execution to subagent**: Use the `subagent` tool in **single mode** with the `implementer` agent:
20
24
  "Execute this task: $ARGUMENTS
21
25