@agentuity/coder 1.0.37

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 (92) hide show
  1. package/README.md +57 -0
  2. package/dist/chain-preview.d.ts +55 -0
  3. package/dist/chain-preview.d.ts.map +1 -0
  4. package/dist/chain-preview.js +472 -0
  5. package/dist/chain-preview.js.map +1 -0
  6. package/dist/client.d.ts +43 -0
  7. package/dist/client.d.ts.map +1 -0
  8. package/dist/client.js +402 -0
  9. package/dist/client.js.map +1 -0
  10. package/dist/commands.d.ts +22 -0
  11. package/dist/commands.d.ts.map +1 -0
  12. package/dist/commands.js +99 -0
  13. package/dist/commands.js.map +1 -0
  14. package/dist/footer.d.ts +34 -0
  15. package/dist/footer.d.ts.map +1 -0
  16. package/dist/footer.js +249 -0
  17. package/dist/footer.js.map +1 -0
  18. package/dist/handlers.d.ts +24 -0
  19. package/dist/handlers.d.ts.map +1 -0
  20. package/dist/handlers.js +83 -0
  21. package/dist/handlers.js.map +1 -0
  22. package/dist/hub-overlay.d.ts +107 -0
  23. package/dist/hub-overlay.d.ts.map +1 -0
  24. package/dist/hub-overlay.js +1794 -0
  25. package/dist/hub-overlay.js.map +1 -0
  26. package/dist/index.d.ts +4 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +1585 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/output-viewer.d.ts +49 -0
  31. package/dist/output-viewer.d.ts.map +1 -0
  32. package/dist/output-viewer.js +389 -0
  33. package/dist/output-viewer.js.map +1 -0
  34. package/dist/overlay.d.ts +40 -0
  35. package/dist/overlay.d.ts.map +1 -0
  36. package/dist/overlay.js +225 -0
  37. package/dist/overlay.js.map +1 -0
  38. package/dist/protocol.d.ts +118 -0
  39. package/dist/protocol.d.ts.map +1 -0
  40. package/dist/protocol.js +3 -0
  41. package/dist/protocol.js.map +1 -0
  42. package/dist/remote-session.d.ts +113 -0
  43. package/dist/remote-session.d.ts.map +1 -0
  44. package/dist/remote-session.js +645 -0
  45. package/dist/remote-session.js.map +1 -0
  46. package/dist/remote-tui.d.ts +40 -0
  47. package/dist/remote-tui.d.ts.map +1 -0
  48. package/dist/remote-tui.js +606 -0
  49. package/dist/remote-tui.js.map +1 -0
  50. package/dist/renderers.d.ts +34 -0
  51. package/dist/renderers.d.ts.map +1 -0
  52. package/dist/renderers.js +669 -0
  53. package/dist/renderers.js.map +1 -0
  54. package/dist/review.d.ts +15 -0
  55. package/dist/review.d.ts.map +1 -0
  56. package/dist/review.js +154 -0
  57. package/dist/review.js.map +1 -0
  58. package/dist/titlebar.d.ts +3 -0
  59. package/dist/titlebar.d.ts.map +1 -0
  60. package/dist/titlebar.js +59 -0
  61. package/dist/titlebar.js.map +1 -0
  62. package/dist/todo/index.d.ts +3 -0
  63. package/dist/todo/index.d.ts.map +1 -0
  64. package/dist/todo/index.js +3 -0
  65. package/dist/todo/index.js.map +1 -0
  66. package/dist/todo/store.d.ts +6 -0
  67. package/dist/todo/store.d.ts.map +1 -0
  68. package/dist/todo/store.js +43 -0
  69. package/dist/todo/store.js.map +1 -0
  70. package/dist/todo/types.d.ts +13 -0
  71. package/dist/todo/types.d.ts.map +1 -0
  72. package/dist/todo/types.js +2 -0
  73. package/dist/todo/types.js.map +1 -0
  74. package/package.json +44 -0
  75. package/src/chain-preview.ts +621 -0
  76. package/src/client.ts +515 -0
  77. package/src/commands.ts +132 -0
  78. package/src/footer.ts +305 -0
  79. package/src/handlers.ts +113 -0
  80. package/src/hub-overlay.ts +2324 -0
  81. package/src/index.ts +1907 -0
  82. package/src/output-viewer.ts +480 -0
  83. package/src/overlay.ts +294 -0
  84. package/src/protocol.ts +157 -0
  85. package/src/remote-session.ts +800 -0
  86. package/src/remote-tui.ts +707 -0
  87. package/src/renderers.ts +740 -0
  88. package/src/review.ts +201 -0
  89. package/src/titlebar.ts +63 -0
  90. package/src/todo/index.ts +2 -0
  91. package/src/todo/store.ts +49 -0
  92. package/src/todo/types.ts +14 -0
package/src/footer.ts ADDED
@@ -0,0 +1,305 @@
1
+ /**
2
+ * Coder footer for the Pi TUI.
3
+ *
4
+ * Uses transparent backgrounds with foreground-only ANSI true-color text.
5
+ * Includes a braille spinner animation when an agent is actively working.
6
+ *
7
+ * Layout:
8
+ * [brand] [branch] > [model/agent] [hub] | [N] label token-stats
9
+ *
10
+ * Observer awareness (ASCII only):
11
+ * [3] SwiftRaven — 3 observers watching, session label "SwiftRaven"
12
+ */
13
+
14
+ import type { ExtensionContext, ReadonlyFooterDataProvider } from '@mariozechner/pi-coding-agent';
15
+
16
+ const RESET = '\x1b[0m';
17
+ const SEP = '>';
18
+
19
+ // ──────────────────────────────────────────────
20
+ // ANSI true-color helper (foreground only)
21
+ // ──────────────────────────────────────────────
22
+
23
+ type RGB = [number, number, number];
24
+
25
+ function fg(color: RGB, text: string): string {
26
+ return `\x1b[38;2;${color[0]};${color[1]};${color[2]}m${text}`;
27
+ }
28
+
29
+ // ──────────────────────────────────────────────
30
+ // Color palette (foreground only, no backgrounds)
31
+ // ──────────────────────────────────────────────
32
+
33
+ const FG_BRAND: RGB = [100, 200, 255];
34
+ const FG_MODEL: RGB = [215, 135, 175];
35
+ const FG_AGENT: RGB = [130, 200, 130];
36
+ const FG_BRANCH: RGB = [150, 180, 150];
37
+ const FG_HUB_OK: RGB = [80, 200, 120];
38
+ const FG_HUB_WARN: RGB = [245, 179, 66];
39
+ const FG_HUB_ERR: RGB = [220, 80, 80];
40
+ const FG_DIM: RGB = [100, 110, 120];
41
+
42
+ type HubStatus = 'connected' | 'reconnecting' | 'offline';
43
+
44
+ /** Observer state provided by the extension's presence tracking. */
45
+ export interface ObserverState {
46
+ /** Number of observers watching this session (excludes lead + sub-agents). */
47
+ count: number;
48
+ /** Human-readable session label (e.g. "SwiftRaven"). */
49
+ label: string;
50
+ }
51
+
52
+ const FG_OBSERVER: RGB = [140, 180, 220];
53
+
54
+ // ──────────────────────────────────────────────
55
+ // Braille spinner
56
+ // ──────────────────────────────────────────────
57
+
58
+ const SPINNER_FRAMES = [
59
+ '\u280B',
60
+ '\u2819',
61
+ '\u2839',
62
+ '\u2838',
63
+ '\u283C',
64
+ '\u2834',
65
+ '\u2826',
66
+ '\u2827',
67
+ '\u2807',
68
+ '\u280F',
69
+ ];
70
+
71
+ // ──────────────────────────────────────────────
72
+ // Footer builder (transparent bg, foreground only)
73
+ // ──────────────────────────────────────────────
74
+
75
+ /** Strip ANSI escape sequences to get visible character count. */
76
+ function visibleLength(str: string): number {
77
+ // eslint-disable-next-line no-control-regex
78
+ return str.replace(/\x1b\[[0-9;]*m/g, '').length;
79
+ }
80
+
81
+ /** Truncate an ANSI-colored string to a maximum visible width. */
82
+ function truncateAnsi(str: string, maxWidth: number): string {
83
+ let visible = 0;
84
+ let i = 0;
85
+ while (i < str.length && visible < maxWidth) {
86
+ if (str[i] === '\x1b') {
87
+ // Skip entire ANSI escape sequence
88
+ const end = str.indexOf('m', i);
89
+ if (end !== -1) {
90
+ i = end + 1;
91
+ } else {
92
+ i++;
93
+ }
94
+ } else {
95
+ visible++;
96
+ i++;
97
+ }
98
+ }
99
+ return str.slice(0, i) + RESET;
100
+ }
101
+
102
+ function buildFooter(left: string, rightText: string, width: number): string {
103
+ // Safety margin for Unicode characters that may be double-width
104
+ const safeWidth = width - 4;
105
+ const leftLen = visibleLength(left);
106
+ const rightLen = visibleLength(rightText);
107
+ const total = leftLen + 1 + rightLen;
108
+
109
+ if (total > safeWidth) {
110
+ const maxLeft = safeWidth - rightLen - 1;
111
+ if (maxLeft > 0) {
112
+ return truncateAnsi(left, maxLeft) + ' ' + rightText;
113
+ }
114
+ return truncateAnsi(left + ' ' + rightText, safeWidth);
115
+ }
116
+
117
+ const gap = safeWidth - leftLen - rightLen;
118
+ return left + ' '.repeat(gap) + rightText;
119
+ }
120
+
121
+ // ──────────────────────────────────────────────
122
+ // Minimal component
123
+ // ──────────────────────────────────────────────
124
+
125
+ class FooterComponent {
126
+ private getText: (width: number) => string;
127
+ private _unsubscribeBranch?: () => void;
128
+
129
+ constructor(
130
+ getText: (width: number) => string,
131
+ footerData: ReadonlyFooterDataProvider,
132
+ cleanupSpinner: () => void
133
+ ) {
134
+ this.getText = getText;
135
+ this._cleanupSpinner = cleanupSpinner;
136
+ this._unsubscribeBranch = footerData.onBranchChange(() => {
137
+ // Triggers TUI refresh
138
+ });
139
+ }
140
+
141
+ private _cleanupSpinner: () => void;
142
+
143
+ render(width: number): string[] {
144
+ const text = this.getText(width);
145
+ // Final safety: ensure line never exceeds terminal width
146
+ if (visibleLength(text) > width) {
147
+ return [truncateAnsi(text, width)];
148
+ }
149
+ return [text];
150
+ }
151
+
152
+ invalidate(): void {
153
+ // no-op
154
+ }
155
+
156
+ dispose(): void {
157
+ this._unsubscribeBranch?.();
158
+ this._cleanupSpinner();
159
+ }
160
+ }
161
+
162
+ // ──────────────────────────────────────────────
163
+ // Token stat formatters
164
+ // ──────────────────────────────────────────────
165
+
166
+ function formatTokens(n: number): string {
167
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
168
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
169
+ return String(n);
170
+ }
171
+
172
+ function formatCost(n: number): string {
173
+ if (n === 0) return '$0.00';
174
+ if (n < 0.01) return `$${n.toFixed(4)}`;
175
+ return `$${n.toFixed(2)}`;
176
+ }
177
+
178
+ // ──────────────────────────────────────────────
179
+ // Public API
180
+ // ──────────────────────────────────────────────
181
+
182
+ /**
183
+ * Set up the Coder footer (transparent bg, foreground-colored text).
184
+ * Call this once with the extension context to replace Pi's default footer.
185
+ *
186
+ * Includes a braille spinner animation when an agent is actively working.
187
+ *
188
+ * @param ctx Extension context with UI access
189
+ * @param getHubStatus Callback that returns current Hub connection status
190
+ * @param getObserverState Optional callback that returns observer count + session label
191
+ */
192
+ export function setupCoderFooter(
193
+ ctx: ExtensionContext,
194
+ getHubStatus: () => HubStatus,
195
+ getObserverState?: () => ObserverState
196
+ ): void {
197
+ if (!ctx.hasUI) return;
198
+
199
+ ctx.ui.setFooter((tui, _theme, footerData) => {
200
+ // Spinner state
201
+ let spinnerTimer: ReturnType<typeof setInterval> | null = null;
202
+ let spinnerFrame = 0;
203
+
204
+ const getText = (width: number): string => {
205
+ // Detect active agent
206
+ const activeAgent = footerData.getExtensionStatuses().get('active_agent');
207
+
208
+ // Start/stop spinner based on agent activity
209
+ if (activeAgent && !spinnerTimer) {
210
+ spinnerTimer = setInterval(() => {
211
+ spinnerFrame = (spinnerFrame + 1) % SPINNER_FRAMES.length;
212
+ tui.requestRender();
213
+ }, 80);
214
+ } else if (!activeAgent && spinnerTimer) {
215
+ clearInterval(spinnerTimer);
216
+ spinnerTimer = null;
217
+ spinnerFrame = 0;
218
+ }
219
+
220
+ // Token stats from session messages
221
+ let inputTokens = 0;
222
+ let outputTokens = 0;
223
+ let totalCost = 0;
224
+ for (const entry of ctx.sessionManager.getBranch()) {
225
+ if (entry.type === 'message') {
226
+ const msg = entry.message as {
227
+ role?: string;
228
+ usage?: { input: number; output: number; cost: { total: number } };
229
+ };
230
+ if (msg.role === 'assistant' && msg.usage) {
231
+ inputTokens += msg.usage.input;
232
+ outputTokens += msg.usage.output;
233
+ totalCost += msg.usage.cost.total;
234
+ }
235
+ }
236
+ }
237
+ const tokenStr = `\u2191${formatTokens(inputTokens)} \u2193${formatTokens(outputTokens)} ${formatCost(totalCost)}`;
238
+
239
+ // LEFT side: brand > branch > model/agent
240
+ const leftParts: string[] = [];
241
+
242
+ // Brand (with spinner)
243
+ const brandChar = spinnerTimer ? SPINNER_FRAMES[spinnerFrame]! : '\u2A3A';
244
+ leftParts.push(fg(FG_BRAND, ` ${brandChar}`));
245
+
246
+ // Branch
247
+ const branch = footerData.getGitBranch();
248
+ if (branch) {
249
+ leftParts.push(' ');
250
+ leftParts.push(fg(FG_BRANCH, branch));
251
+ }
252
+
253
+ // Model or active agent
254
+ leftParts.push(fg(FG_DIM, ` ${SEP} `));
255
+ if (activeAgent) {
256
+ leftParts.push(fg(FG_AGENT, activeAgent));
257
+ } else {
258
+ const modelId = ctx.model ? String((ctx.model as { id?: string }).id ?? '?') : '?';
259
+ leftParts.push(fg(FG_MODEL, modelId));
260
+ }
261
+
262
+ const left = leftParts.join('');
263
+
264
+ // RIGHT side: hub status + observer info + token stats
265
+ const rightParts: string[] = [];
266
+ const hubStatus = getHubStatus();
267
+ if (hubStatus === 'connected') {
268
+ rightParts.push(fg(FG_HUB_OK, 'Hub'));
269
+ } else if (hubStatus === 'reconnecting') {
270
+ rightParts.push(fg(FG_HUB_WARN, 'Hub...'));
271
+ } else {
272
+ rightParts.push(fg(FG_HUB_ERR, 'Hub Off'));
273
+ }
274
+
275
+ // Observer awareness (ASCII only, no emojis)
276
+ if (getObserverState && hubStatus === 'connected') {
277
+ const obs = getObserverState();
278
+ if (obs.count > 0 || obs.label) {
279
+ rightParts.push(fg(FG_DIM, ' | '));
280
+ if (obs.count > 0) {
281
+ rightParts.push(fg(FG_OBSERVER, `[${obs.count}]`));
282
+ if (obs.label) rightParts.push(' ');
283
+ }
284
+ if (obs.label) {
285
+ rightParts.push(fg(FG_OBSERVER, obs.label));
286
+ }
287
+ }
288
+ }
289
+
290
+ rightParts.push(fg(FG_DIM, ` ${tokenStr}`) + RESET);
291
+ const rightText = rightParts.join('');
292
+
293
+ return buildFooter(left, rightText, width);
294
+ };
295
+
296
+ const cleanupSpinner = (): void => {
297
+ if (spinnerTimer) {
298
+ clearInterval(spinnerTimer);
299
+ spinnerTimer = null;
300
+ }
301
+ };
302
+
303
+ return new FooterComponent(getText, footerData, cleanupSpinner);
304
+ });
305
+ }
@@ -0,0 +1,113 @@
1
+ import type { HubAction } from './protocol.ts';
2
+
3
+ export interface ActionResult {
4
+ block?: { block: true; reason: string };
5
+ returnValue?: unknown;
6
+ systemPrompt?: string;
7
+ systemPromptMode?: 'replace' | 'prefix' | 'suffix';
8
+ // undefined means ACK (proceed normally)
9
+ }
10
+
11
+ /** Minimal UI surface used by action handlers — avoids a hard dep on pi-coding-agent. */
12
+ interface ActionContext {
13
+ ui?: {
14
+ notify(message: string, level?: 'info' | 'warning' | 'error'): void;
15
+ confirm(title: string, message: string): Promise<boolean>;
16
+ setStatus(key: string, text?: string): void;
17
+ };
18
+ sendUserMessage?: (message: string, options?: { deliverAs?: 'followUp' }) => void;
19
+ }
20
+
21
+ export async function processActions(
22
+ actions: HubAction[],
23
+ ctx: ActionContext
24
+ ): Promise<ActionResult> {
25
+ let result: ActionResult = {};
26
+
27
+ for (const action of actions) {
28
+ switch (action.action) {
29
+ case 'ACK':
30
+ // Terminal: proceed normally
31
+ result = {};
32
+ break;
33
+
34
+ case 'BLOCK':
35
+ // Terminal: block
36
+ result = { block: { block: true, reason: action.reason } };
37
+ break;
38
+
39
+ case 'RETURN':
40
+ // Terminal: return a specific result
41
+ result = { returnValue: action.result };
42
+ break;
43
+
44
+ case 'NOTIFY':
45
+ // Side effect: show notification, continue
46
+ if (ctx?.ui) {
47
+ ctx.ui.notify(action.message, action.level ?? 'info');
48
+ }
49
+ break;
50
+
51
+ case 'STATUS':
52
+ // Side effect: set status, continue
53
+ if (ctx?.ui) {
54
+ ctx.ui.setStatus(action.key, action.text);
55
+ }
56
+ break;
57
+
58
+ case 'CONFIRM': {
59
+ // Gate: if user denies, stop and block
60
+ if (ctx?.ui) {
61
+ const confirmed = await ctx.ui.confirm(action.title, action.message);
62
+ if (!confirmed) {
63
+ return {
64
+ block: {
65
+ block: true,
66
+ reason: action.deny_reason ?? 'Denied by user',
67
+ },
68
+ };
69
+ }
70
+ } else {
71
+ // No UI available — block by default for safety
72
+ result = {
73
+ block: {
74
+ block: true,
75
+ reason: action.deny_reason ?? 'Confirmation required but no UI available',
76
+ },
77
+ };
78
+ }
79
+ break;
80
+ }
81
+
82
+ case 'SYSTEM_PROMPT':
83
+ // System prompt injection — store for before_agent_start handler
84
+ result = {
85
+ ...result,
86
+ systemPrompt: action.systemPrompt,
87
+ systemPromptMode: action.mode,
88
+ };
89
+ break;
90
+
91
+ case 'INJECT_MESSAGE': {
92
+ const content = action.message?.content?.trim();
93
+ if (!content) break;
94
+
95
+ if (action.message?.role === 'user') {
96
+ if (ctx.sendUserMessage) {
97
+ ctx.sendUserMessage(content, { deliverAs: 'followUp' });
98
+ } else if (ctx.ui) {
99
+ ctx.ui.notify(content, 'info');
100
+ }
101
+ break;
102
+ }
103
+
104
+ if (ctx.ui) {
105
+ ctx.ui.notify(content, 'info');
106
+ }
107
+ break;
108
+ }
109
+ }
110
+ }
111
+
112
+ return result;
113
+ }