@teammates/cli 0.1.0 → 0.2.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.
- package/README.md +31 -22
- package/dist/adapter.d.ts +1 -1
- package/dist/adapter.js +68 -56
- package/dist/adapter.test.js +34 -21
- package/dist/adapters/cli-proxy.d.ts +11 -4
- package/dist/adapters/cli-proxy.js +176 -162
- package/dist/adapters/copilot.d.ts +50 -0
- package/dist/adapters/copilot.js +210 -0
- package/dist/adapters/echo.d.ts +2 -2
- package/dist/adapters/echo.js +2 -1
- package/dist/adapters/echo.test.js +4 -2
- package/dist/cli-utils.d.ts +21 -0
- package/dist/cli-utils.js +74 -0
- package/dist/cli-utils.test.d.ts +1 -0
- package/dist/cli-utils.test.js +179 -0
- package/dist/cli.js +3160 -961
- package/dist/compact.d.ts +39 -0
- package/dist/compact.js +269 -0
- package/dist/compact.test.d.ts +1 -0
- package/dist/compact.test.js +198 -0
- package/dist/console/ansi.d.ts +18 -0
- package/dist/console/ansi.js +20 -0
- package/dist/console/ansi.test.d.ts +1 -0
- package/dist/console/ansi.test.js +50 -0
- package/dist/console/dropdown.d.ts +23 -0
- package/dist/console/dropdown.js +63 -0
- package/dist/console/file-drop.d.ts +59 -0
- package/dist/console/file-drop.js +186 -0
- package/dist/console/file-drop.test.d.ts +1 -0
- package/dist/console/file-drop.test.js +145 -0
- package/dist/console/index.d.ts +22 -0
- package/dist/console/index.js +23 -0
- package/dist/console/interactive-readline.d.ts +65 -0
- package/dist/console/interactive-readline.js +132 -0
- package/dist/console/markdown-table.d.ts +17 -0
- package/dist/console/markdown-table.js +270 -0
- package/dist/console/markdown-table.test.d.ts +1 -0
- package/dist/console/markdown-table.test.js +130 -0
- package/dist/console/mutable-output.d.ts +21 -0
- package/dist/console/mutable-output.js +51 -0
- package/dist/console/paste-handler.d.ts +63 -0
- package/dist/console/paste-handler.js +177 -0
- package/dist/console/prompt-box.d.ts +55 -0
- package/dist/console/prompt-box.js +120 -0
- package/dist/console/prompt-input.d.ts +136 -0
- package/dist/console/prompt-input.js +618 -0
- package/dist/console/startup.d.ts +20 -0
- package/dist/console/startup.js +138 -0
- package/dist/console/startup.test.d.ts +1 -0
- package/dist/console/startup.test.js +41 -0
- package/dist/console/wordwheel.d.ts +75 -0
- package/dist/console/wordwheel.js +123 -0
- package/dist/dropdown.js +4 -21
- package/dist/index.d.ts +5 -5
- package/dist/index.js +3 -3
- package/dist/onboard.d.ts +24 -0
- package/dist/onboard.js +174 -11
- package/dist/orchestrator.d.ts +8 -11
- package/dist/orchestrator.js +33 -81
- package/dist/orchestrator.test.js +59 -79
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +56 -12
- package/dist/registry.test.js +57 -13
- package/dist/theme.d.ts +56 -0
- package/dist/theme.js +54 -0
- package/dist/types.d.ts +18 -13
- package/package.json +8 -3
- package/template/CROSS-TEAM.md +2 -2
- package/template/PROTOCOL.md +72 -15
- package/template/README.md +2 -2
- package/template/TEMPLATE.md +118 -15
- package/template/example/SOUL.md +2 -1
- package/template/example/WISDOM.md +9 -0
- package/dist/adapters/codex.d.ts +0 -50
- package/dist/adapters/codex.js +0 -213
- package/template/example/MEMORIES.md +0 -26
package/dist/cli.js
CHANGED
|
@@ -7,21 +7,37 @@
|
|
|
7
7
|
* teammates --adapter codex Use a specific agent adapter
|
|
8
8
|
* teammates --dir <path> Override .teammates/ location
|
|
9
9
|
*/
|
|
10
|
+
import { spawn as cpSpawn, exec as execCb, execSync, } from "node:child_process";
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { mkdir, readdir, rm, stat, unlink } from "node:fs/promises";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join, resolve } from "node:path";
|
|
10
15
|
import { createInterface } from "node:readline";
|
|
11
|
-
import { Writable } from "node:stream";
|
|
12
|
-
import { resolve, join } from "node:path";
|
|
13
|
-
import { stat, mkdir, readdir } from "node:fs/promises";
|
|
14
|
-
import { execSync, exec as execCb } from "node:child_process";
|
|
15
|
-
import { statSync, readdirSync } from "node:fs";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
17
|
const execAsync = promisify(execCb);
|
|
18
|
+
import { App, ChatView, Control, concat, esc, Interview, pen, renderMarkdown, StyledText, stripAnsi, } from "@teammates/consolonia";
|
|
18
19
|
import chalk from "chalk";
|
|
19
20
|
import ora from "ora";
|
|
20
|
-
import { Orchestrator } from "./orchestrator.js";
|
|
21
|
-
import { EchoAdapter } from "./adapters/echo.js";
|
|
22
21
|
import { CliProxyAdapter, PRESETS } from "./adapters/cli-proxy.js";
|
|
23
|
-
import {
|
|
24
|
-
import {
|
|
22
|
+
import { CopilotAdapter } from "./adapters/copilot.js";
|
|
23
|
+
import { EchoAdapter } from "./adapters/echo.js";
|
|
24
|
+
import { findAtMention, isImagePath, relativeTime, wrapLine, } from "./cli-utils.js";
|
|
25
|
+
import { compactEpisodic } from "./compact.js";
|
|
26
|
+
import { PromptInput } from "./console/prompt-input.js";
|
|
27
|
+
import { buildTitle } from "./console/startup.js";
|
|
28
|
+
import { buildAdaptationPrompt, copyTemplateFiles, getOnboardingPrompt, importTeammates, } from "./onboard.js";
|
|
29
|
+
import { Orchestrator } from "./orchestrator.js";
|
|
30
|
+
import { colorToHex, theme } from "./theme.js";
|
|
31
|
+
// ─── Version ─────────────────────────────────────────────────────────
|
|
32
|
+
const PKG_VERSION = (() => {
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf-8"));
|
|
35
|
+
return pkg.version ?? "0.0.0";
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return "0.0.0";
|
|
39
|
+
}
|
|
40
|
+
})();
|
|
25
41
|
// ─── Argument parsing ────────────────────────────────────────────────
|
|
26
42
|
const args = process.argv.slice(2);
|
|
27
43
|
function getFlag(name) {
|
|
@@ -61,7 +77,9 @@ async function findTeammatesDir() {
|
|
|
61
77
|
if (s.isDirectory())
|
|
62
78
|
return candidate;
|
|
63
79
|
}
|
|
64
|
-
catch {
|
|
80
|
+
catch {
|
|
81
|
+
/* keep looking */
|
|
82
|
+
}
|
|
65
83
|
const parent = resolve(dir, "..");
|
|
66
84
|
if (parent === dir)
|
|
67
85
|
break;
|
|
@@ -72,6 +90,12 @@ async function findTeammatesDir() {
|
|
|
72
90
|
function resolveAdapter(name) {
|
|
73
91
|
if (name === "echo")
|
|
74
92
|
return new EchoAdapter();
|
|
93
|
+
// GitHub Copilot SDK adapter
|
|
94
|
+
if (name === "copilot") {
|
|
95
|
+
return new CopilotAdapter({
|
|
96
|
+
model: modelOverride,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
75
99
|
// All other adapters go through the CLI proxy
|
|
76
100
|
if (PRESETS[name]) {
|
|
77
101
|
return new CliProxyAdapter({
|
|
@@ -80,22 +104,11 @@ function resolveAdapter(name) {
|
|
|
80
104
|
extraFlags: agentPassthrough,
|
|
81
105
|
});
|
|
82
106
|
}
|
|
83
|
-
const available = ["echo", ...Object.keys(PRESETS)].join(", ");
|
|
107
|
+
const available = ["echo", "copilot", ...Object.keys(PRESETS)].join(", ");
|
|
84
108
|
console.error(chalk.red(`Unknown adapter: ${name}`));
|
|
85
109
|
console.error(`Available adapters: ${available}`);
|
|
86
110
|
process.exit(1);
|
|
87
111
|
}
|
|
88
|
-
function relativeTime(date) {
|
|
89
|
-
const diff = Date.now() - date.getTime();
|
|
90
|
-
const secs = Math.floor(diff / 1000);
|
|
91
|
-
if (secs < 60)
|
|
92
|
-
return `${secs}s ago`;
|
|
93
|
-
const mins = Math.floor(secs / 60);
|
|
94
|
-
if (mins < 60)
|
|
95
|
-
return `${mins}m ago`;
|
|
96
|
-
const hrs = Math.floor(mins / 60);
|
|
97
|
-
return `${hrs}h ago`;
|
|
98
|
-
}
|
|
99
112
|
const SERVICE_REGISTRY = {
|
|
100
113
|
recall: {
|
|
101
114
|
package: "@teammates/recall",
|
|
@@ -108,18 +121,306 @@ const SERVICE_REGISTRY = {
|
|
|
108
121
|
"",
|
|
109
122
|
"1. Verify `teammates-recall --help` works. If it does, great. If not, figure out the correct path to the binary (check recall/package.json bin field) and note it.",
|
|
110
123
|
"2. Read .teammates/PROTOCOL.md and .teammates/CROSS-TEAM.md.",
|
|
111
|
-
|
|
124
|
+
'3. If recall is not already documented there, add a short section explaining that `teammates-recall` is now available for semantic memory search, with basic usage (e.g. `teammates-recall search "query"`).',
|
|
112
125
|
"4. Check each teammate's SOUL.md (under .teammates/*/SOUL.md). If a teammate's role involves memory or search, note in their SOUL.md that recall is installed and available.",
|
|
113
126
|
"5. Do NOT modify code files — only update .teammates/ markdown files.",
|
|
114
127
|
].join("\n"),
|
|
115
128
|
},
|
|
116
129
|
};
|
|
130
|
+
// WordwheelItem is now DropdownItem from @teammates/consolonia
|
|
131
|
+
// ── Themed pen shortcuts ────────────────────────────────────────────
|
|
132
|
+
//
|
|
133
|
+
// Thin wrappers that read from the active theme() at call time, so
|
|
134
|
+
// every styled span picks up the current palette automatically.
|
|
135
|
+
const tp = {
|
|
136
|
+
accent: (s) => pen.fg(theme().accent)(s),
|
|
137
|
+
accentBright: (s) => pen.fg(theme().accentBright)(s),
|
|
138
|
+
accentDim: (s) => pen.fg(theme().accentDim)(s),
|
|
139
|
+
text: (s) => pen.fg(theme().text)(s),
|
|
140
|
+
muted: (s) => pen.fg(theme().textMuted)(s),
|
|
141
|
+
dim: (s) => pen.fg(theme().textDim)(s),
|
|
142
|
+
success: (s) => pen.fg(theme().success)(s),
|
|
143
|
+
warning: (s) => pen.fg(theme().warning)(s),
|
|
144
|
+
error: (s) => pen.fg(theme().error)(s),
|
|
145
|
+
info: (s) => pen.fg(theme().info)(s),
|
|
146
|
+
bold: (s) => pen.bold.fg(theme().text)(s),
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Custom banner widget that plays a reveal animation inside the
|
|
150
|
+
* consolonia rendering loop (alternate screen already active).
|
|
151
|
+
*
|
|
152
|
+
* Phases:
|
|
153
|
+
* 1. Reveal "teammates" letter by letter in block font
|
|
154
|
+
* 2. Collapse to "TM" + stats panel
|
|
155
|
+
* 3. Fade in teammate roster
|
|
156
|
+
* 4. Fade in command reference
|
|
157
|
+
*/
|
|
158
|
+
class AnimatedBanner extends Control {
|
|
159
|
+
_lines = [];
|
|
160
|
+
_info;
|
|
161
|
+
_phase = "idle";
|
|
162
|
+
_inner;
|
|
163
|
+
_timer = null;
|
|
164
|
+
_onDirty = null;
|
|
165
|
+
// Spelling state
|
|
166
|
+
_word = "teammates";
|
|
167
|
+
_charIndex = 0;
|
|
168
|
+
_builtTop = "";
|
|
169
|
+
_builtBot = "";
|
|
170
|
+
_versionStr = ` v${PKG_VERSION}`;
|
|
171
|
+
_versionIndex = 0;
|
|
172
|
+
// Roster/command reveal state
|
|
173
|
+
_revealIndex = 0;
|
|
174
|
+
/** When true, the animation pauses after roster reveal (before commands). */
|
|
175
|
+
_held = false;
|
|
176
|
+
// The final lines (built once, revealed progressively)
|
|
177
|
+
_finalLines = [];
|
|
178
|
+
// Line index where roster starts and commands start
|
|
179
|
+
_rosterStart = 0;
|
|
180
|
+
_commandsStart = 0;
|
|
181
|
+
static GLYPHS = {
|
|
182
|
+
t: ["▀█▀", " █ "],
|
|
183
|
+
e: ["█▀▀", "██▄"],
|
|
184
|
+
a: ["▄▀█", "█▀█"],
|
|
185
|
+
m: ["█▀▄▀█", "█ ▀ █"],
|
|
186
|
+
s: ["█▀", "▄█"],
|
|
187
|
+
};
|
|
188
|
+
constructor(info) {
|
|
189
|
+
super();
|
|
190
|
+
this._info = info;
|
|
191
|
+
this._inner = new StyledText({ lines: [], wrap: true });
|
|
192
|
+
this.addChild(this._inner);
|
|
193
|
+
this._buildFinalLines();
|
|
194
|
+
}
|
|
195
|
+
/** Set a callback that fires when the banner needs a re-render. */
|
|
196
|
+
set onDirty(fn) {
|
|
197
|
+
this._onDirty = fn;
|
|
198
|
+
}
|
|
199
|
+
/** Start the animation sequence. */
|
|
200
|
+
start() {
|
|
201
|
+
this._phase = "spelling";
|
|
202
|
+
this._charIndex = 0;
|
|
203
|
+
this._builtTop = "";
|
|
204
|
+
this._builtBot = "";
|
|
205
|
+
this._tick();
|
|
206
|
+
}
|
|
207
|
+
_buildFinalLines() {
|
|
208
|
+
const info = this._info;
|
|
209
|
+
const [tmTop, tmBot] = buildTitle("tm");
|
|
210
|
+
const tmPad = " ".repeat(tmTop.length);
|
|
211
|
+
const gap = " ";
|
|
212
|
+
const lines = [];
|
|
213
|
+
// TM logo row 1 + adapter info
|
|
214
|
+
lines.push(concat(tp.accent(tmTop), tp.text(gap + info.adapterName), tp.muted(` · ${info.teammateCount} teammate${info.teammateCount === 1 ? "" : "s"}`), tp.muted(` · v${PKG_VERSION}`)));
|
|
215
|
+
// TM logo row 2 + cwd
|
|
216
|
+
lines.push(concat(tp.accent(tmBot), tp.muted(gap + info.cwd)));
|
|
217
|
+
// Recall status (indented to align with info above)
|
|
218
|
+
lines.push(info.recallInstalled
|
|
219
|
+
? concat(tp.text(tmPad + gap), tp.success("● "), tp.success("recall"), tp.muted(" installed"))
|
|
220
|
+
: concat(tp.text(tmPad + gap), tp.warning("○ "), tp.warning("recall"), tp.muted(" not installed")));
|
|
221
|
+
// blank
|
|
222
|
+
lines.push("");
|
|
223
|
+
this._rosterStart = lines.length;
|
|
224
|
+
// Teammate roster
|
|
225
|
+
for (const t of info.teammates) {
|
|
226
|
+
lines.push(concat(tp.accent(" ● "), tp.accent(`@${t.name}`.padEnd(14)), tp.muted(t.role)));
|
|
227
|
+
}
|
|
228
|
+
// blank
|
|
229
|
+
lines.push("");
|
|
230
|
+
this._commandsStart = lines.length;
|
|
231
|
+
// Command reference (must match printBanner normal-mode layout)
|
|
232
|
+
const col1 = [
|
|
233
|
+
["@mention", "assign to teammate"],
|
|
234
|
+
["text", "auto-route task"],
|
|
235
|
+
["[image]", "drag & drop images"],
|
|
236
|
+
];
|
|
237
|
+
const col2 = [
|
|
238
|
+
["/status", "teammates & queue"],
|
|
239
|
+
["/compact", "compact memory"],
|
|
240
|
+
["/retro", "run retrospective"],
|
|
241
|
+
];
|
|
242
|
+
const col3 = [
|
|
243
|
+
[
|
|
244
|
+
info.recallInstalled ? "/copy" : "/install",
|
|
245
|
+
info.recallInstalled ? "copy session text" : "add a service",
|
|
246
|
+
],
|
|
247
|
+
["/help", "all commands"],
|
|
248
|
+
["/exit", "exit session"],
|
|
249
|
+
];
|
|
250
|
+
for (let i = 0; i < col1.length; i++) {
|
|
251
|
+
lines.push(concat(tp.accent(` ${col1[i][0].padEnd(12)}`), tp.muted(col1[i][1].padEnd(22)), tp.accent(col2[i][0].padEnd(12)), tp.muted(col2[i][1].padEnd(22)), tp.accent(col3[i][0].padEnd(12)), tp.muted(col3[i][1])));
|
|
252
|
+
}
|
|
253
|
+
this._finalLines = lines;
|
|
254
|
+
}
|
|
255
|
+
_tick() {
|
|
256
|
+
switch (this._phase) {
|
|
257
|
+
case "spelling": {
|
|
258
|
+
const ch = this._word[this._charIndex];
|
|
259
|
+
const g = AnimatedBanner.GLYPHS[ch];
|
|
260
|
+
if (g) {
|
|
261
|
+
if (this._builtTop.length > 0) {
|
|
262
|
+
this._builtTop += " ";
|
|
263
|
+
this._builtBot += " ";
|
|
264
|
+
}
|
|
265
|
+
this._builtTop += g[0];
|
|
266
|
+
this._builtBot += g[1];
|
|
267
|
+
}
|
|
268
|
+
this._lines = [
|
|
269
|
+
concat(tp.accent(this._builtTop)),
|
|
270
|
+
concat(tp.accent(this._builtBot)),
|
|
271
|
+
];
|
|
272
|
+
this._apply();
|
|
273
|
+
this._charIndex++;
|
|
274
|
+
if (this._charIndex >= this._word.length) {
|
|
275
|
+
this._phase = "version";
|
|
276
|
+
this._versionIndex = 0;
|
|
277
|
+
this._schedule(60);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
this._schedule(60);
|
|
281
|
+
}
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
case "version": {
|
|
285
|
+
// Type out version string character by character on the bottom row
|
|
286
|
+
this._versionIndex++;
|
|
287
|
+
const partial = this._versionStr.slice(0, this._versionIndex);
|
|
288
|
+
this._lines = [
|
|
289
|
+
concat(tp.accent(this._builtTop)),
|
|
290
|
+
concat(tp.accent(this._builtBot), tp.muted(partial)),
|
|
291
|
+
];
|
|
292
|
+
this._apply();
|
|
293
|
+
if (this._versionIndex >= this._versionStr.length) {
|
|
294
|
+
this._phase = "pause";
|
|
295
|
+
this._schedule(600);
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
this._schedule(60);
|
|
299
|
+
}
|
|
300
|
+
break;
|
|
301
|
+
}
|
|
302
|
+
case "pause": {
|
|
303
|
+
// Brief pause before transitioning to compact view
|
|
304
|
+
this._phase = "compact";
|
|
305
|
+
this._schedule(800);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
case "compact": {
|
|
309
|
+
// Switch to TM + stats — show first 4 lines of final
|
|
310
|
+
this._lines = this._finalLines.slice(0, 4);
|
|
311
|
+
this._apply();
|
|
312
|
+
this._phase = "roster";
|
|
313
|
+
this._revealIndex = 0;
|
|
314
|
+
this._schedule(80);
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
case "roster": {
|
|
318
|
+
// Reveal roster lines one at a time
|
|
319
|
+
const end = this._rosterStart + this._revealIndex + 1;
|
|
320
|
+
this._lines = [
|
|
321
|
+
...this._finalLines.slice(0, this._rosterStart),
|
|
322
|
+
...this._finalLines.slice(this._rosterStart, end),
|
|
323
|
+
];
|
|
324
|
+
this._apply();
|
|
325
|
+
this._revealIndex++;
|
|
326
|
+
const rosterCount = this._commandsStart - 1 - this._rosterStart; // -1 for blank line
|
|
327
|
+
if (this._revealIndex >= rosterCount) {
|
|
328
|
+
if (this._held) {
|
|
329
|
+
// Pause here until releaseHold() is called
|
|
330
|
+
this._phase = "roster-held";
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
this._phase = "commands";
|
|
334
|
+
this._revealIndex = 0;
|
|
335
|
+
this._schedule(80);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
this._schedule(40);
|
|
340
|
+
}
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "commands": {
|
|
344
|
+
// Add the blank line between roster and commands, then reveal commands
|
|
345
|
+
const rosterEnd = this._commandsStart; // includes the blank line
|
|
346
|
+
const cmdEnd = this._commandsStart + this._revealIndex + 1;
|
|
347
|
+
this._lines = [
|
|
348
|
+
...this._finalLines.slice(0, rosterEnd),
|
|
349
|
+
...this._finalLines.slice(this._commandsStart, cmdEnd),
|
|
350
|
+
];
|
|
351
|
+
this._apply();
|
|
352
|
+
this._revealIndex++;
|
|
353
|
+
const cmdCount = this._finalLines.length - this._commandsStart;
|
|
354
|
+
if (this._revealIndex >= cmdCount) {
|
|
355
|
+
this._phase = "done";
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
this._schedule(30);
|
|
359
|
+
}
|
|
360
|
+
break;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
_apply() {
|
|
365
|
+
this._inner.lines = this._lines;
|
|
366
|
+
this.invalidate();
|
|
367
|
+
if (this._onDirty)
|
|
368
|
+
this._onDirty();
|
|
369
|
+
}
|
|
370
|
+
_schedule(ms) {
|
|
371
|
+
this._timer = setTimeout(() => {
|
|
372
|
+
this._timer = null;
|
|
373
|
+
this._tick();
|
|
374
|
+
}, ms);
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Hold the animation — it will pause after the roster phase and
|
|
378
|
+
* not reveal the command reference until releaseHold() is called.
|
|
379
|
+
*/
|
|
380
|
+
hold() {
|
|
381
|
+
this._held = true;
|
|
382
|
+
}
|
|
383
|
+
/**
|
|
384
|
+
* Release the hold and continue to the commands phase.
|
|
385
|
+
* If the animation already reached the hold point, it resumes immediately.
|
|
386
|
+
*/
|
|
387
|
+
releaseHold() {
|
|
388
|
+
this._held = false;
|
|
389
|
+
// If we're waiting at the hold point, resume
|
|
390
|
+
if (this._phase === "roster-held") {
|
|
391
|
+
this._phase = "commands";
|
|
392
|
+
this._revealIndex = 0;
|
|
393
|
+
this._schedule(80);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
/** Cancel any pending animation timer. */
|
|
397
|
+
dispose() {
|
|
398
|
+
if (this._timer) {
|
|
399
|
+
clearTimeout(this._timer);
|
|
400
|
+
this._timer = null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
// ── Layout delegation ───────────────────────────────────────────
|
|
404
|
+
measure(constraint) {
|
|
405
|
+
const size = this._inner.measure(constraint);
|
|
406
|
+
this.desiredSize = size;
|
|
407
|
+
return size;
|
|
408
|
+
}
|
|
409
|
+
arrange(rect) {
|
|
410
|
+
this.bounds = rect;
|
|
411
|
+
this._inner.arrange(rect);
|
|
412
|
+
}
|
|
413
|
+
render(ctx) {
|
|
414
|
+
this._inner.render(ctx);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
117
417
|
// ─── REPL ────────────────────────────────────────────────────────────
|
|
118
418
|
class TeammatesREPL {
|
|
119
419
|
orchestrator;
|
|
120
420
|
adapter;
|
|
121
|
-
|
|
122
|
-
|
|
421
|
+
input;
|
|
422
|
+
chatView;
|
|
423
|
+
app;
|
|
123
424
|
commands = new Map();
|
|
124
425
|
lastResult = null;
|
|
125
426
|
lastResults = new Map();
|
|
@@ -145,199 +446,1242 @@ class TeammatesREPL {
|
|
|
145
446
|
}
|
|
146
447
|
adapterName;
|
|
147
448
|
teammatesDir;
|
|
449
|
+
recallWatchProcess = null;
|
|
148
450
|
taskQueue = [];
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
|
|
153
|
-
/** True while a task is being dispatched — prevents concurrent dispatches from pasted text. */
|
|
154
|
-
dispatching = false;
|
|
451
|
+
/** Per-agent active tasks — one per agent running in parallel. */
|
|
452
|
+
agentActive = new Map();
|
|
453
|
+
/** Per-agent drain locks — prevents double-draining a single agent. */
|
|
454
|
+
agentDrainLocks = new Map();
|
|
155
455
|
/** Stored pasted text keyed by paste number, expanded on Enter. */
|
|
156
456
|
pastedTexts = new Map();
|
|
157
|
-
|
|
457
|
+
pasteCounter = 0;
|
|
158
458
|
wordwheelItems = [];
|
|
159
459
|
wordwheelIndex = -1; // -1 = no selection, 0+ = highlighted row
|
|
460
|
+
escPending = false; // true after first ESC, waiting for second
|
|
461
|
+
escTimer = null;
|
|
462
|
+
ctrlcPending = false; // true after first Ctrl+C, waiting for second
|
|
463
|
+
ctrlcTimer = null;
|
|
464
|
+
lastCleanedOutput = ""; // last teammate output for clipboard copy
|
|
465
|
+
dispatching = false;
|
|
466
|
+
autoApproveHandoffs = false;
|
|
467
|
+
/** Pending handoffs awaiting user approval. */
|
|
468
|
+
pendingHandoffs = [];
|
|
469
|
+
/** Pending retro proposals awaiting user approval. */
|
|
470
|
+
pendingRetroProposals = [];
|
|
471
|
+
/** Maps reply action IDs to their context (teammate + message). */
|
|
472
|
+
_replyContexts = new Map();
|
|
473
|
+
/** Quoted reply text to expand on next submit. */
|
|
474
|
+
_pendingQuotedReply = null;
|
|
475
|
+
defaultFooter = null; // cached default footer content
|
|
476
|
+
// ── Animated status tracker ─────────────────────────────────────
|
|
477
|
+
activeTasks = new Map();
|
|
478
|
+
statusTimer = null;
|
|
479
|
+
statusFrame = 0;
|
|
480
|
+
statusRotateIndex = 0;
|
|
481
|
+
statusRotateTimer = null;
|
|
482
|
+
static SPINNER = [
|
|
483
|
+
"⠋",
|
|
484
|
+
"⠙",
|
|
485
|
+
"⠹",
|
|
486
|
+
"⠸",
|
|
487
|
+
"⠼",
|
|
488
|
+
"⠴",
|
|
489
|
+
"⠦",
|
|
490
|
+
"⠧",
|
|
491
|
+
"⠇",
|
|
492
|
+
"⠏",
|
|
493
|
+
];
|
|
160
494
|
constructor(adapterName) {
|
|
161
495
|
this.adapterName = adapterName;
|
|
162
496
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
async promptOnboarding(adapter) {
|
|
169
|
-
const cwd = process.cwd();
|
|
170
|
-
const teammatesDir = join(cwd, ".teammates");
|
|
171
|
-
const termWidth = process.stdout.columns || 100;
|
|
172
|
-
console.log();
|
|
173
|
-
this.printLogo([
|
|
174
|
-
chalk.bold("Teammates") + chalk.gray(" v0.1.0"),
|
|
175
|
-
chalk.yellow("No .teammates/ directory found"),
|
|
176
|
-
chalk.gray(cwd),
|
|
177
|
-
]);
|
|
178
|
-
console.log();
|
|
179
|
-
console.log(chalk.gray("─".repeat(termWidth)));
|
|
180
|
-
console.log();
|
|
181
|
-
console.log(chalk.white(" Set up teammates for this project?\n"));
|
|
182
|
-
console.log(chalk.cyan(" 1") + chalk.gray(") ") +
|
|
183
|
-
chalk.white("Run onboarding") +
|
|
184
|
-
chalk.gray(" — analyze this codebase and create .teammates/"));
|
|
185
|
-
console.log(chalk.cyan(" 2") + chalk.gray(") ") +
|
|
186
|
-
chalk.white("Solo mode") +
|
|
187
|
-
chalk.gray(` — use ${this.adapterName} without teammates`));
|
|
188
|
-
console.log(chalk.cyan(" 3") + chalk.gray(") ") +
|
|
189
|
-
chalk.white("Exit"));
|
|
190
|
-
console.log();
|
|
191
|
-
const choice = await this.askChoice("Pick an option (1/2/3): ", ["1", "2", "3"]);
|
|
192
|
-
if (choice === "3") {
|
|
193
|
-
console.log(chalk.gray(" Goodbye."));
|
|
194
|
-
return null;
|
|
497
|
+
/** Show the prompt with the fenced border. */
|
|
498
|
+
showPrompt() {
|
|
499
|
+
if (this.chatView) {
|
|
500
|
+
// ChatView is always visible — just refresh
|
|
501
|
+
this.app.refresh();
|
|
195
502
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
console.log();
|
|
199
|
-
console.log(chalk.green(" ✔") + chalk.gray(` Created ${teammatesDir}`));
|
|
200
|
-
console.log(chalk.gray(` Running in solo mode — all tasks go to ${this.adapterName}.`));
|
|
201
|
-
console.log(chalk.gray(" Run /init later to set up teammates."));
|
|
202
|
-
console.log();
|
|
203
|
-
return teammatesDir;
|
|
503
|
+
else {
|
|
504
|
+
this.input.activate();
|
|
204
505
|
}
|
|
205
|
-
// choice === "1": Run onboarding via the agent
|
|
206
|
-
await mkdir(teammatesDir, { recursive: true });
|
|
207
|
-
await this.runOnboardingAgent(adapter, cwd);
|
|
208
|
-
return teammatesDir;
|
|
209
506
|
}
|
|
210
|
-
/**
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
const tempConfig = {
|
|
228
|
-
name: this.adapterName,
|
|
229
|
-
role: "Onboarding agent",
|
|
230
|
-
soul: "",
|
|
231
|
-
memories: "",
|
|
232
|
-
dailyLogs: [],
|
|
233
|
-
ownership: { primary: [], secondary: [] },
|
|
234
|
-
};
|
|
235
|
-
const sessionId = await adapter.startSession(tempConfig);
|
|
236
|
-
const spinner = ora({
|
|
237
|
-
text: chalk.blue(this.adapterName) + chalk.gray(" is analyzing your codebase..."),
|
|
238
|
-
spinner: "dots",
|
|
239
|
-
}).start();
|
|
240
|
-
try {
|
|
241
|
-
const result = await adapter.executeTask(sessionId, tempConfig, onboardingPrompt);
|
|
242
|
-
spinner.stop();
|
|
243
|
-
this.printAgentOutput(result.rawOutput);
|
|
244
|
-
if (result.success) {
|
|
245
|
-
console.log(chalk.green(" ✔ Onboarding complete!"));
|
|
246
|
-
}
|
|
247
|
-
else {
|
|
248
|
-
console.log(chalk.yellow(" ⚠ Onboarding finished with issues: " + result.summary));
|
|
507
|
+
/** Start or update the animated status tracker above the prompt. */
|
|
508
|
+
startStatusAnimation() {
|
|
509
|
+
if (this.statusTimer)
|
|
510
|
+
return; // already running
|
|
511
|
+
this.statusFrame = 0;
|
|
512
|
+
this.statusRotateIndex = 0;
|
|
513
|
+
this.renderStatusFrame();
|
|
514
|
+
// Animate spinner at ~80ms
|
|
515
|
+
this.statusTimer = setInterval(() => {
|
|
516
|
+
this.statusFrame++;
|
|
517
|
+
this.renderStatusFrame();
|
|
518
|
+
}, 80);
|
|
519
|
+
// Rotate through teammates every 3 seconds
|
|
520
|
+
this.statusRotateTimer = setInterval(() => {
|
|
521
|
+
if (this.activeTasks.size > 1) {
|
|
522
|
+
this.statusRotateIndex =
|
|
523
|
+
(this.statusRotateIndex + 1) % this.activeTasks.size;
|
|
249
524
|
}
|
|
525
|
+
}, 3000);
|
|
526
|
+
}
|
|
527
|
+
/** Stop the status animation and clear the status line. */
|
|
528
|
+
stopStatusAnimation() {
|
|
529
|
+
if (this.statusTimer) {
|
|
530
|
+
clearInterval(this.statusTimer);
|
|
531
|
+
this.statusTimer = null;
|
|
250
532
|
}
|
|
251
|
-
|
|
252
|
-
|
|
533
|
+
if (this.statusRotateTimer) {
|
|
534
|
+
clearInterval(this.statusRotateTimer);
|
|
535
|
+
this.statusRotateTimer = null;
|
|
253
536
|
}
|
|
254
|
-
if (
|
|
255
|
-
|
|
537
|
+
if (this.chatView) {
|
|
538
|
+
this.chatView.setProgress(null);
|
|
539
|
+
this.app.refresh();
|
|
256
540
|
}
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
541
|
+
else {
|
|
542
|
+
this.input.setStatus(null);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
/** Render one frame of the status animation. */
|
|
546
|
+
renderStatusFrame() {
|
|
547
|
+
if (this.activeTasks.size === 0)
|
|
548
|
+
return;
|
|
549
|
+
const entries = Array.from(this.activeTasks.values());
|
|
550
|
+
const idx = this.statusRotateIndex % entries.length;
|
|
551
|
+
const { teammate, task } = entries[idx];
|
|
552
|
+
const spinChar = TeammatesREPL.SPINNER[this.statusFrame % TeammatesREPL.SPINNER.length];
|
|
553
|
+
const taskPreview = task.length > 50 ? `${task.slice(0, 47)}...` : task;
|
|
554
|
+
const queueInfo = this.activeTasks.size > 1 ? ` (${idx + 1}/${this.activeTasks.size})` : "";
|
|
555
|
+
if (this.chatView) {
|
|
556
|
+
// Strip newlines and truncate task text for single-line display
|
|
557
|
+
const cleanTask = task.replace(/[\r\n]+/g, " ").trim();
|
|
558
|
+
const maxLen = Math.max(20, (process.stdout.columns || 80) - teammate.length - 10);
|
|
559
|
+
const taskText = cleanTask.length > maxLen
|
|
560
|
+
? `${cleanTask.slice(0, maxLen - 1)}…`
|
|
561
|
+
: cleanTask;
|
|
562
|
+
const queueTag = this.activeTasks.size > 1
|
|
563
|
+
? ` (${idx + 1}/${this.activeTasks.size})`
|
|
564
|
+
: "";
|
|
565
|
+
this.chatView.setProgress(concat(tp.accent(`${spinChar} ${teammate}… `), tp.muted(taskText + queueTag)));
|
|
566
|
+
this.app.refresh();
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
// Mostly bright blue, periodically flicker to dark blue
|
|
570
|
+
const spinColor = this.statusFrame % 8 === 0 ? chalk.blue : chalk.blueBright;
|
|
571
|
+
const line = ` ${spinColor(spinChar)} ` +
|
|
572
|
+
chalk.bold(teammate) +
|
|
573
|
+
chalk.gray(`… ${taskPreview}`) +
|
|
574
|
+
(queueInfo ? chalk.gray(queueInfo) : "");
|
|
575
|
+
this.input.setStatus(line);
|
|
264
576
|
}
|
|
265
|
-
catch { /* dir might not exist if onboarding failed badly */ }
|
|
266
|
-
console.log();
|
|
267
577
|
}
|
|
268
578
|
/**
|
|
269
|
-
*
|
|
579
|
+
* Print the user's message as an inverted block in the feed.
|
|
580
|
+
* White text on dark background, right-aligned indicator.
|
|
270
581
|
*/
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
582
|
+
_userBg = { r: 25, g: 25, b: 25, a: 255 };
|
|
583
|
+
/** Feed a line with the user message background, padded to full width. */
|
|
584
|
+
feedUserLine(spans) {
|
|
585
|
+
if (!this.chatView)
|
|
586
|
+
return;
|
|
587
|
+
const termW = (process.stdout.columns || 80) - 1; // -1 for scrollbar
|
|
588
|
+
// Calculate visible length of spans
|
|
589
|
+
let len = 0;
|
|
590
|
+
for (const seg of spans)
|
|
591
|
+
len += seg.text.length;
|
|
592
|
+
const pad = Math.max(0, termW - len);
|
|
593
|
+
const padded = concat(spans, pen.fg(this._userBg).bg(this._userBg)(" ".repeat(pad)));
|
|
594
|
+
this.chatView.appendStyledToFeed(padded);
|
|
595
|
+
}
|
|
596
|
+
/** Word-wrap text to maxWidth, breaking at spaces. */
|
|
597
|
+
wrapLine(text, maxWidth) {
|
|
598
|
+
return wrapLine(text, maxWidth);
|
|
599
|
+
}
|
|
600
|
+
printUserMessage(text) {
|
|
601
|
+
if (this.chatView) {
|
|
602
|
+
const bg = this._userBg;
|
|
603
|
+
const t = theme();
|
|
604
|
+
const termW = (process.stdout.columns || 80) - 1; // -1 for scrollbar
|
|
605
|
+
const allLines = text.split("\n");
|
|
606
|
+
// Separate non-quote lines from blockquote lines (> prefix)
|
|
607
|
+
// Find contiguous blockquote regions and fence them with empty lines
|
|
608
|
+
const rendered = [];
|
|
609
|
+
let inQuote = false;
|
|
610
|
+
for (const line of allLines) {
|
|
611
|
+
const isQuote = line.startsWith("> ") || line === ">";
|
|
612
|
+
if (isQuote && !inQuote) {
|
|
613
|
+
rendered.push({ type: "text", content: "" }); // empty line before quotes
|
|
614
|
+
inQuote = true;
|
|
615
|
+
}
|
|
616
|
+
else if (!isQuote && inQuote) {
|
|
617
|
+
rendered.push({ type: "text", content: "" }); // empty line after quotes
|
|
618
|
+
inQuote = false;
|
|
619
|
+
}
|
|
620
|
+
if (isQuote) {
|
|
621
|
+
rendered.push({
|
|
622
|
+
type: "quote",
|
|
623
|
+
content: line.startsWith("> ") ? line.slice(2) : "",
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
rendered.push({ type: "text", content: line });
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// Render first line with "User: " label
|
|
631
|
+
const label = "user: ";
|
|
632
|
+
const first = rendered.shift();
|
|
633
|
+
if (first) {
|
|
634
|
+
if (first.type === "text") {
|
|
635
|
+
const firstWrapW = termW - label.length;
|
|
636
|
+
const firstWrapped = this.wrapLine(first.content, firstWrapW);
|
|
637
|
+
// First wrapped segment gets the label
|
|
638
|
+
const seg0 = firstWrapped.shift() ?? "";
|
|
639
|
+
const pad0 = Math.max(0, termW - label.length - seg0.length);
|
|
640
|
+
this.chatView.appendStyledToFeed(concat(pen.fg(t.accent).bg(bg)(label), pen.fg(t.text).bg(bg)(seg0 + " ".repeat(pad0))));
|
|
641
|
+
// Remaining wrapped segments are indented to align with content
|
|
642
|
+
for (const wl of firstWrapped) {
|
|
643
|
+
this.feedUserLine(concat(pen.fg(t.text).bg(bg)(wl)));
|
|
280
644
|
}
|
|
281
|
-
|
|
282
|
-
|
|
645
|
+
}
|
|
646
|
+
else {
|
|
647
|
+
// First line is a quote (unusual but handle it)
|
|
648
|
+
const pad = Math.max(0, termW - label.length);
|
|
649
|
+
this.chatView.appendStyledToFeed(concat(pen.fg(t.accent).bg(bg)(label + " ".repeat(pad))));
|
|
650
|
+
// Re-add to render as quote
|
|
651
|
+
rendered.unshift(first);
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
// Render remaining lines
|
|
655
|
+
for (const entry of rendered) {
|
|
656
|
+
if (entry.type === "quote") {
|
|
657
|
+
const prefix = "│ ";
|
|
658
|
+
const wrapWidth = termW - prefix.length;
|
|
659
|
+
const wrapped = this.wrapLine(entry.content, wrapWidth);
|
|
660
|
+
for (const wl of wrapped) {
|
|
661
|
+
const pad = Math.max(0, termW - prefix.length - wl.length);
|
|
662
|
+
this.chatView.appendStyledToFeed(concat(pen.fg(t.textDim).bg(bg)(prefix), pen.fg(t.textMuted).bg(bg)(wl + " ".repeat(pad))));
|
|
283
663
|
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
664
|
+
}
|
|
665
|
+
else {
|
|
666
|
+
const wrapWidth = termW;
|
|
667
|
+
const wrapped = this.wrapLine(entry.content, wrapWidth);
|
|
668
|
+
for (const wl of wrapped) {
|
|
669
|
+
this.feedUserLine(concat(pen.fg(t.text).bg(bg)(wl)));
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
this.app.refresh();
|
|
674
|
+
return;
|
|
675
|
+
}
|
|
676
|
+
const termWidth = process.stdout.columns || 100;
|
|
677
|
+
const maxWidth = Math.min(termWidth - 4, 80);
|
|
678
|
+
const lines = text.split("\n");
|
|
679
|
+
console.log();
|
|
680
|
+
for (const line of lines) {
|
|
681
|
+
// Truncate long lines
|
|
682
|
+
const display = line.length > maxWidth ? `${line.slice(0, maxWidth - 1)}…` : line;
|
|
683
|
+
const padded = display + " ".repeat(Math.max(0, maxWidth - stripAnsi(display).length));
|
|
684
|
+
console.log(` ${chalk.bgGray.white(` ${padded} `)}`);
|
|
685
|
+
}
|
|
686
|
+
console.log();
|
|
288
687
|
}
|
|
289
|
-
// ─── Display helpers ──────────────────────────────────────────────
|
|
290
688
|
/**
|
|
291
|
-
*
|
|
689
|
+
* Route text input to the right teammate and queue it for execution.
|
|
690
|
+
* Returns immediately — the task runs in the background via drainQueue.
|
|
292
691
|
*/
|
|
293
|
-
printLogo(infoLines) {
|
|
294
|
-
const pad = (i) => infoLines[i] ? " " + infoLines[i] : "";
|
|
295
|
-
console.log(chalk.cyan(" ▐▛▀▀▀▀▀▀▜▌") + pad(0));
|
|
296
|
-
console.log(chalk.cyan(" ▐▌") + " " + chalk.cyan("▐▌") + pad(1));
|
|
297
|
-
console.log(chalk.cyan(" ▐▌") + " 🧬 " + chalk.cyan("▐▌") + pad(2));
|
|
298
|
-
console.log(chalk.cyan(" ▐▌") + " " + chalk.cyan("▐▌") + pad(3));
|
|
299
|
-
console.log(chalk.cyan(" ▐▙▄▄▄▄▄▄▟▌"));
|
|
300
|
-
}
|
|
301
692
|
/**
|
|
302
|
-
*
|
|
693
|
+
* Write a line to the chat feed.
|
|
694
|
+
* Accepts a plain string or a StyledSpan for colored output.
|
|
303
695
|
*/
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
696
|
+
feedLine(text = "") {
|
|
697
|
+
if (this.chatView) {
|
|
698
|
+
if (typeof text === "string") {
|
|
699
|
+
this.chatView.appendToFeed(text);
|
|
700
|
+
}
|
|
701
|
+
else {
|
|
702
|
+
this.chatView.appendStyledToFeed(text);
|
|
703
|
+
}
|
|
307
704
|
return;
|
|
308
|
-
const cleaned = raw.replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/, "").trim();
|
|
309
|
-
if (cleaned) {
|
|
310
|
-
console.log(cleaned);
|
|
311
705
|
}
|
|
312
|
-
console
|
|
706
|
+
// Fallback: convert StyledSpan to plain text for console
|
|
707
|
+
if (typeof text !== "string") {
|
|
708
|
+
console.log(text.map((s) => s.text).join(""));
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
console.log(text);
|
|
712
|
+
}
|
|
313
713
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
714
|
+
/** Render markdown text to the feed using the consolonia markdown widget. */
|
|
715
|
+
feedMarkdown(source) {
|
|
716
|
+
const t = theme();
|
|
717
|
+
const width = process.stdout.columns || 80;
|
|
718
|
+
const lines = renderMarkdown(source, {
|
|
719
|
+
width: width - 3, // -2 for indent, -1 for scrollbar
|
|
720
|
+
indent: " ",
|
|
721
|
+
theme: {
|
|
722
|
+
text: { fg: t.textMuted },
|
|
723
|
+
bold: { fg: t.text, bold: true },
|
|
724
|
+
italic: { fg: t.textMuted, italic: true },
|
|
725
|
+
boldItalic: { fg: t.text, bold: true, italic: true },
|
|
726
|
+
code: { fg: t.accentDim },
|
|
727
|
+
h1: { fg: t.accent, bold: true },
|
|
728
|
+
h2: { fg: t.accent, bold: true },
|
|
729
|
+
h3: { fg: t.accent },
|
|
730
|
+
codeBlockChrome: { fg: t.textDim },
|
|
731
|
+
codeBlock: { fg: t.success },
|
|
732
|
+
blockquote: { fg: t.textMuted, italic: true },
|
|
733
|
+
listMarker: { fg: t.accent },
|
|
734
|
+
tableBorder: { fg: t.textDim },
|
|
735
|
+
tableHeader: { fg: t.text, bold: true },
|
|
736
|
+
hr: { fg: t.textDim },
|
|
737
|
+
link: { fg: t.accent, underline: true },
|
|
738
|
+
linkUrl: { fg: t.textMuted },
|
|
739
|
+
strikethrough: { fg: t.textMuted, strikethrough: true },
|
|
740
|
+
checkbox: { fg: t.accent },
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
for (const line of lines) {
|
|
744
|
+
// Convert markdown Line (Seg[]) to StyledSpan, preserving all style flags
|
|
745
|
+
const styledSpan = line.map((seg) => ({
|
|
746
|
+
text: seg.text,
|
|
747
|
+
style: seg.style,
|
|
748
|
+
}));
|
|
749
|
+
styledSpan.__brand = "StyledSpan";
|
|
750
|
+
this.feedLine(styledSpan);
|
|
323
751
|
}
|
|
324
|
-
return result;
|
|
325
752
|
}
|
|
326
|
-
|
|
327
|
-
|
|
753
|
+
/** Render handoff blocks with approve/reject actions. */
|
|
754
|
+
/** Helper to create a branded StyledSpan from segments. */
|
|
755
|
+
makeSpan(...segs) {
|
|
756
|
+
const s = segs;
|
|
757
|
+
s.__brand = "StyledSpan";
|
|
758
|
+
return s;
|
|
328
759
|
}
|
|
329
|
-
|
|
330
|
-
|
|
760
|
+
/** Word-wrap a string to fit within maxWidth. */
|
|
761
|
+
wordWrap(text, maxWidth) {
|
|
762
|
+
const words = text.split(" ");
|
|
763
|
+
const lines = [];
|
|
764
|
+
let current = "";
|
|
765
|
+
for (const word of words) {
|
|
766
|
+
if (current.length === 0) {
|
|
767
|
+
current = word;
|
|
768
|
+
}
|
|
769
|
+
else if (current.length + 1 + word.length <= maxWidth) {
|
|
770
|
+
current += ` ${word}`;
|
|
771
|
+
}
|
|
772
|
+
else {
|
|
773
|
+
lines.push(current);
|
|
774
|
+
current = word;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (current)
|
|
778
|
+
lines.push(current);
|
|
779
|
+
return lines.length > 0 ? lines : [""];
|
|
331
780
|
}
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
781
|
+
renderHandoffs(_from, handoffs) {
|
|
782
|
+
const t = theme();
|
|
783
|
+
const names = this.orchestrator.listTeammates();
|
|
784
|
+
const avail = (process.stdout.columns || 80) - 4; // -4 for " │ " + " │"
|
|
785
|
+
const boxW = Math.max(40, Math.round(avail * 0.6));
|
|
786
|
+
const innerW = boxW - 4; // space inside │ _ content _ │
|
|
787
|
+
for (let i = 0; i < handoffs.length; i++) {
|
|
788
|
+
const h = handoffs[i];
|
|
789
|
+
const isValid = names.includes(h.to);
|
|
790
|
+
const handoffId = `handoff-${Date.now()}-${i}`;
|
|
791
|
+
const chrome = isValid ? t.accentDim : t.error;
|
|
792
|
+
// Top border with label
|
|
793
|
+
this.feedLine();
|
|
794
|
+
const label = ` handoff → @${h.to} `;
|
|
795
|
+
const topFill = Math.max(0, boxW - 2 - label.length);
|
|
796
|
+
this.feedLine(this.makeSpan({
|
|
797
|
+
text: ` ┌${label}${"─".repeat(topFill)}┐`,
|
|
798
|
+
style: { fg: chrome },
|
|
799
|
+
}));
|
|
800
|
+
// Task body — word-wrap each paragraph line
|
|
801
|
+
for (const rawLine of h.task.split("\n")) {
|
|
802
|
+
const wrapped = rawLine.length === 0 ? [""] : this.wordWrap(rawLine, innerW);
|
|
803
|
+
for (const wl of wrapped) {
|
|
804
|
+
const pad = Math.max(0, innerW - wl.length);
|
|
805
|
+
this.feedLine(this.makeSpan({ text: " │ ", style: { fg: chrome } }, { text: wl + " ".repeat(pad), style: { fg: t.textMuted } }, { text: " │", style: { fg: chrome } }));
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
// Bottom border
|
|
809
|
+
this.feedLine(this.makeSpan({
|
|
810
|
+
text: ` └${"─".repeat(Math.max(0, boxW - 2))}┘`,
|
|
811
|
+
style: { fg: chrome },
|
|
812
|
+
}));
|
|
813
|
+
if (!isValid) {
|
|
814
|
+
this.feedLine(tp.error(` ✖ Unknown teammate: @${h.to}`));
|
|
815
|
+
}
|
|
816
|
+
else if (this.autoApproveHandoffs) {
|
|
817
|
+
this.taskQueue.push({ type: "agent", teammate: h.to, task: h.task });
|
|
818
|
+
this.feedLine(tp.muted(" automatically approved"));
|
|
819
|
+
this.kickDrain();
|
|
820
|
+
}
|
|
821
|
+
else if (this.chatView) {
|
|
822
|
+
const actionIdx = this.chatView.feedLineCount;
|
|
823
|
+
this.chatView.appendActionList([
|
|
824
|
+
{
|
|
825
|
+
id: `approve-${handoffId}`,
|
|
826
|
+
normalStyle: this.makeSpan({
|
|
827
|
+
text: " [approve]",
|
|
828
|
+
style: { fg: t.textDim },
|
|
829
|
+
}),
|
|
830
|
+
hoverStyle: this.makeSpan({
|
|
831
|
+
text: " [approve]",
|
|
832
|
+
style: { fg: t.accent },
|
|
833
|
+
}),
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
id: `reject-${handoffId}`,
|
|
837
|
+
normalStyle: this.makeSpan({
|
|
838
|
+
text: " [reject]",
|
|
839
|
+
style: { fg: t.textDim },
|
|
840
|
+
}),
|
|
841
|
+
hoverStyle: this.makeSpan({
|
|
842
|
+
text: " [reject]",
|
|
843
|
+
style: { fg: t.accent },
|
|
844
|
+
}),
|
|
845
|
+
},
|
|
846
|
+
]);
|
|
847
|
+
this.pendingHandoffs.push({
|
|
848
|
+
id: handoffId,
|
|
849
|
+
envelope: h,
|
|
850
|
+
approveIdx: actionIdx,
|
|
851
|
+
rejectIdx: actionIdx,
|
|
852
|
+
});
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
// Show global approval options as dropdown when there are pending handoffs
|
|
856
|
+
this.showHandoffDropdown();
|
|
857
|
+
this.refreshView();
|
|
858
|
+
}
|
|
859
|
+
/** Show/hide the handoff approval dropdown based on pending handoffs. */
|
|
860
|
+
showHandoffDropdown() {
|
|
861
|
+
if (!this.chatView)
|
|
862
|
+
return;
|
|
863
|
+
if (this.pendingHandoffs.length > 0) {
|
|
864
|
+
const items = [];
|
|
865
|
+
if (this.pendingHandoffs.length === 1) {
|
|
866
|
+
items.push({
|
|
867
|
+
label: "approve",
|
|
868
|
+
description: `approve handoff to @${this.pendingHandoffs[0].envelope.to}`,
|
|
869
|
+
completion: "/approve",
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
else {
|
|
873
|
+
items.push({
|
|
874
|
+
label: "approve",
|
|
875
|
+
description: `approve ${this.pendingHandoffs.length} handoffs`,
|
|
876
|
+
completion: "/approve",
|
|
877
|
+
});
|
|
878
|
+
}
|
|
879
|
+
items.push({
|
|
880
|
+
label: "always approve",
|
|
881
|
+
description: "auto-approve future handoffs",
|
|
882
|
+
completion: "/always-approve",
|
|
883
|
+
});
|
|
884
|
+
if (this.pendingHandoffs.length === 1) {
|
|
885
|
+
items.push({
|
|
886
|
+
label: "reject",
|
|
887
|
+
description: `reject handoff to @${this.pendingHandoffs[0].envelope.to}`,
|
|
888
|
+
completion: "/reject",
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
items.push({
|
|
893
|
+
label: "reject",
|
|
894
|
+
description: `reject ${this.pendingHandoffs.length} handoffs`,
|
|
895
|
+
completion: "/reject",
|
|
896
|
+
});
|
|
897
|
+
}
|
|
898
|
+
this.chatView.showDropdown(items);
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
this.chatView.hideDropdown();
|
|
902
|
+
}
|
|
903
|
+
this.refreshView();
|
|
904
|
+
}
|
|
905
|
+
/** Handle handoff approve/reject actions. */
|
|
906
|
+
handleHandoffAction(actionId) {
|
|
907
|
+
const approveMatch = actionId.match(/^approve-(.+)$/);
|
|
908
|
+
if (approveMatch) {
|
|
909
|
+
const hId = approveMatch[1];
|
|
910
|
+
const idx = this.pendingHandoffs.findIndex((h) => h.id === hId);
|
|
911
|
+
if (idx >= 0 && this.chatView) {
|
|
912
|
+
const h = this.pendingHandoffs.splice(idx, 1)[0];
|
|
913
|
+
this.taskQueue.push({
|
|
914
|
+
type: "agent",
|
|
915
|
+
teammate: h.envelope.to,
|
|
916
|
+
task: h.envelope.task,
|
|
917
|
+
});
|
|
918
|
+
this.chatView.updateFeedLine(h.approveIdx, this.makeSpan({ text: " approved", style: { fg: theme().success } }));
|
|
919
|
+
this.kickDrain();
|
|
920
|
+
this.showHandoffDropdown();
|
|
921
|
+
}
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
const rejectMatch = actionId.match(/^reject-(.+)$/);
|
|
925
|
+
if (rejectMatch) {
|
|
926
|
+
const hId = rejectMatch[1];
|
|
927
|
+
const idx = this.pendingHandoffs.findIndex((h) => h.id === hId);
|
|
928
|
+
if (idx >= 0 && this.chatView) {
|
|
929
|
+
const h = this.pendingHandoffs.splice(idx, 1)[0];
|
|
930
|
+
this.chatView.updateFeedLine(h.approveIdx, this.makeSpan({ text: " rejected", style: { fg: theme().error } }));
|
|
931
|
+
this.showHandoffDropdown();
|
|
932
|
+
}
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
/** Handle bulk handoff actions. */
|
|
937
|
+
handleBulkHandoff(action) {
|
|
938
|
+
if (!this.chatView)
|
|
939
|
+
return;
|
|
940
|
+
const t = theme();
|
|
941
|
+
const isApprove = action === "Approve all" || action === "Always approve";
|
|
942
|
+
if (action === "Always approve") {
|
|
943
|
+
this.autoApproveHandoffs = true;
|
|
944
|
+
}
|
|
945
|
+
for (const h of this.pendingHandoffs) {
|
|
946
|
+
if (isApprove) {
|
|
947
|
+
this.taskQueue.push({
|
|
948
|
+
type: "agent",
|
|
949
|
+
teammate: h.envelope.to,
|
|
950
|
+
task: h.envelope.task,
|
|
951
|
+
});
|
|
952
|
+
const label = action === "Always approve"
|
|
953
|
+
? " automatically approved"
|
|
954
|
+
: " approved";
|
|
955
|
+
this.chatView.updateFeedLine(h.approveIdx, this.makeSpan({ text: label, style: { fg: t.success } }));
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
this.chatView.updateFeedLine(h.approveIdx, this.makeSpan({ text: " rejected", style: { fg: t.error } }));
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
this.pendingHandoffs = [];
|
|
962
|
+
if (isApprove)
|
|
963
|
+
this.kickDrain();
|
|
964
|
+
this.showHandoffDropdown();
|
|
965
|
+
}
|
|
966
|
+
// ─── Retro Phase 2: proposal approval ─────────────────────────
|
|
967
|
+
/** Parse retro proposals from agent output and render approval UI. */
|
|
968
|
+
handleRetroResult(result) {
|
|
969
|
+
const raw = result.rawOutput ?? "";
|
|
970
|
+
const proposals = this.parseRetroProposals(raw);
|
|
971
|
+
if (proposals.length === 0)
|
|
972
|
+
return;
|
|
973
|
+
const t = theme();
|
|
974
|
+
const teammate = result.teammate;
|
|
975
|
+
const retroId = `retro-${Date.now()}`;
|
|
976
|
+
this.feedLine();
|
|
977
|
+
this.feedLine(concat(tp.accent(` ${proposals.length} SOUL.md proposal${proposals.length > 1 ? "s" : ""}`), tp.muted(" — approve or reject each:")));
|
|
978
|
+
for (let i = 0; i < proposals.length; i++) {
|
|
979
|
+
const p = proposals[i];
|
|
980
|
+
const pId = `${retroId}-${i}`;
|
|
981
|
+
this.feedLine();
|
|
982
|
+
this.feedLine(tp.text(` Proposal ${i + 1}: ${p.title}`));
|
|
983
|
+
this.feedLine(tp.muted(` Section: ${p.section}`));
|
|
984
|
+
if (p.before === "(new entry)") {
|
|
985
|
+
this.feedLine(tp.muted(" Before: (new entry)"));
|
|
986
|
+
}
|
|
987
|
+
else {
|
|
988
|
+
this.feedLine(tp.muted(` Before: ${p.before}`));
|
|
989
|
+
}
|
|
990
|
+
this.feedLine(concat(tp.muted(" After: "), tp.text(p.after)));
|
|
991
|
+
this.feedLine(tp.muted(` Why: ${p.why}`));
|
|
992
|
+
if (this.chatView) {
|
|
993
|
+
const actionIdx = this.chatView.feedLineCount;
|
|
994
|
+
this.chatView.appendActionList([
|
|
995
|
+
{
|
|
996
|
+
id: `retro-approve-${pId}`,
|
|
997
|
+
normalStyle: this.makeSpan({
|
|
998
|
+
text: " [approve]",
|
|
999
|
+
style: { fg: t.textDim },
|
|
1000
|
+
}),
|
|
1001
|
+
hoverStyle: this.makeSpan({
|
|
1002
|
+
text: " [approve]",
|
|
1003
|
+
style: { fg: t.accent },
|
|
1004
|
+
}),
|
|
1005
|
+
},
|
|
1006
|
+
{
|
|
1007
|
+
id: `retro-reject-${pId}`,
|
|
1008
|
+
normalStyle: this.makeSpan({
|
|
1009
|
+
text: " [reject]",
|
|
1010
|
+
style: { fg: t.textDim },
|
|
1011
|
+
}),
|
|
1012
|
+
hoverStyle: this.makeSpan({
|
|
1013
|
+
text: " [reject]",
|
|
1014
|
+
style: { fg: t.accent },
|
|
1015
|
+
}),
|
|
1016
|
+
},
|
|
1017
|
+
]);
|
|
1018
|
+
this.pendingRetroProposals.push({
|
|
1019
|
+
id: pId,
|
|
1020
|
+
teammate,
|
|
1021
|
+
index: i + 1,
|
|
1022
|
+
title: p.title,
|
|
1023
|
+
section: p.section,
|
|
1024
|
+
before: p.before,
|
|
1025
|
+
after: p.after,
|
|
1026
|
+
why: p.why,
|
|
1027
|
+
actionIdx,
|
|
1028
|
+
});
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
this.feedLine();
|
|
1032
|
+
this.showRetroDropdown();
|
|
1033
|
+
this.refreshView();
|
|
1034
|
+
}
|
|
1035
|
+
/** Parse Proposal N blocks from retro output. */
|
|
1036
|
+
parseRetroProposals(text) {
|
|
1037
|
+
const proposals = [];
|
|
1038
|
+
// Match **Proposal N: title** blocks
|
|
1039
|
+
const proposalPattern = /\*\*Proposal\s+\d+[:.]\s*(.+?)\*\*/gi;
|
|
1040
|
+
let match;
|
|
1041
|
+
const positions = [];
|
|
1042
|
+
while ((match = proposalPattern.exec(text)) !== null) {
|
|
1043
|
+
positions.push({ title: match[1].trim(), start: match.index });
|
|
1044
|
+
}
|
|
1045
|
+
for (let i = 0; i < positions.length; i++) {
|
|
1046
|
+
const end = i + 1 < positions.length ? positions[i + 1].start : text.length;
|
|
1047
|
+
const block = text.slice(positions[i].start, end);
|
|
1048
|
+
const section = this.extractField(block, "Section") || "Unknown";
|
|
1049
|
+
const before = this.extractField(block, "Before") || "(new entry)";
|
|
1050
|
+
const after = this.extractField(block, "After") || "";
|
|
1051
|
+
const why = this.extractField(block, "Why") || "";
|
|
1052
|
+
if (after) {
|
|
1053
|
+
proposals.push({
|
|
1054
|
+
title: positions[i].title,
|
|
1055
|
+
section,
|
|
1056
|
+
before,
|
|
1057
|
+
after,
|
|
1058
|
+
why,
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
return proposals;
|
|
1063
|
+
}
|
|
1064
|
+
/** Extract a **Field:** value from a proposal block. */
|
|
1065
|
+
extractField(block, field) {
|
|
1066
|
+
// Match "- **Field:** value" or "**Field:** value" across potential line breaks
|
|
1067
|
+
const pattern = new RegExp(`\\*\\*${field}:\\*\\*\\s*(.+?)(?=\\n\\s*[-*]\\s*\\*\\*|\\n\\s*\\n|$)`, "is");
|
|
1068
|
+
const m = block.match(pattern);
|
|
1069
|
+
if (!m)
|
|
1070
|
+
return "";
|
|
1071
|
+
// Clean up: remove backticks and trim
|
|
1072
|
+
return m[1].trim().replace(/^`+|`+$/g, "");
|
|
1073
|
+
}
|
|
1074
|
+
/** Show/hide the retro approval dropdown based on pending proposals. */
|
|
1075
|
+
showRetroDropdown() {
|
|
1076
|
+
if (!this.chatView)
|
|
1077
|
+
return;
|
|
1078
|
+
if (this.pendingRetroProposals.length > 0 &&
|
|
1079
|
+
this.pendingHandoffs.length === 0) {
|
|
1080
|
+
const n = this.pendingRetroProposals.length;
|
|
1081
|
+
const items = [];
|
|
1082
|
+
items.push({
|
|
1083
|
+
label: "approve all",
|
|
1084
|
+
description: `approve ${n} SOUL.md proposal${n > 1 ? "s" : ""}`,
|
|
1085
|
+
completion: "/approve-retro",
|
|
1086
|
+
});
|
|
1087
|
+
items.push({
|
|
1088
|
+
label: "reject all",
|
|
1089
|
+
description: `reject ${n} SOUL.md proposal${n > 1 ? "s" : ""}`,
|
|
1090
|
+
completion: "/reject-retro",
|
|
1091
|
+
});
|
|
1092
|
+
this.chatView.showDropdown(items);
|
|
1093
|
+
}
|
|
1094
|
+
else if (this.pendingHandoffs.length === 0) {
|
|
1095
|
+
this.chatView.hideDropdown();
|
|
1096
|
+
}
|
|
1097
|
+
this.refreshView();
|
|
1098
|
+
}
|
|
1099
|
+
/** Handle retro approve/reject actions (individual clicks). */
|
|
1100
|
+
handleRetroAction(actionId) {
|
|
1101
|
+
const approveMatch = actionId.match(/^retro-approve-(.+)$/);
|
|
1102
|
+
if (approveMatch) {
|
|
1103
|
+
const pId = approveMatch[1];
|
|
1104
|
+
const idx = this.pendingRetroProposals.findIndex((p) => p.id === pId);
|
|
1105
|
+
if (idx >= 0 && this.chatView) {
|
|
1106
|
+
const p = this.pendingRetroProposals.splice(idx, 1)[0];
|
|
1107
|
+
this.chatView.updateFeedLine(p.actionIdx, this.makeSpan({
|
|
1108
|
+
text: " approved",
|
|
1109
|
+
style: { fg: theme().success },
|
|
1110
|
+
}));
|
|
1111
|
+
this.queueRetroApply(p.teammate, [p]);
|
|
1112
|
+
this.showRetroDropdown();
|
|
1113
|
+
}
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
const rejectMatch = actionId.match(/^retro-reject-(.+)$/);
|
|
1117
|
+
if (rejectMatch) {
|
|
1118
|
+
const pId = rejectMatch[1];
|
|
1119
|
+
const idx = this.pendingRetroProposals.findIndex((p) => p.id === pId);
|
|
1120
|
+
if (idx >= 0 && this.chatView) {
|
|
1121
|
+
const p = this.pendingRetroProposals.splice(idx, 1)[0];
|
|
1122
|
+
this.chatView.updateFeedLine(p.actionIdx, this.makeSpan({ text: " rejected", style: { fg: theme().error } }));
|
|
1123
|
+
this.showRetroDropdown();
|
|
1124
|
+
}
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
/** Handle bulk retro approve/reject. */
|
|
1129
|
+
handleBulkRetro(action) {
|
|
1130
|
+
if (!this.chatView)
|
|
1131
|
+
return;
|
|
1132
|
+
const t = theme();
|
|
1133
|
+
const isApprove = action === "Approve all";
|
|
1134
|
+
const grouped = new Map();
|
|
1135
|
+
for (const p of this.pendingRetroProposals) {
|
|
1136
|
+
if (isApprove) {
|
|
1137
|
+
this.chatView.updateFeedLine(p.actionIdx, this.makeSpan({ text: " approved", style: { fg: t.success } }));
|
|
1138
|
+
const list = grouped.get(p.teammate) || [];
|
|
1139
|
+
list.push(p);
|
|
1140
|
+
grouped.set(p.teammate, list);
|
|
1141
|
+
}
|
|
1142
|
+
else {
|
|
1143
|
+
this.chatView.updateFeedLine(p.actionIdx, this.makeSpan({ text: " rejected", style: { fg: t.error } }));
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
1146
|
+
if (isApprove) {
|
|
1147
|
+
for (const [teammate, proposals] of grouped) {
|
|
1148
|
+
this.queueRetroApply(teammate, proposals);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
this.pendingRetroProposals = [];
|
|
1152
|
+
this.showRetroDropdown();
|
|
1153
|
+
}
|
|
1154
|
+
/** Queue a follow-up task for the teammate to apply approved SOUL.md changes. */
|
|
1155
|
+
queueRetroApply(teammate, proposals) {
|
|
1156
|
+
const changes = proposals
|
|
1157
|
+
.map((p) => `- **Proposal ${p.index}: ${p.title}**\n - Section: ${p.section}\n - Before: ${p.before}\n - After: ${p.after}`)
|
|
1158
|
+
.join("\n\n");
|
|
1159
|
+
const applyPrompt = `The user approved the following SOUL.md changes from your retrospective. Apply them now.
|
|
1160
|
+
|
|
1161
|
+
**Edit your SOUL.md file** (\`.teammates/${teammate}/SOUL.md\`) to incorporate these changes:
|
|
1162
|
+
|
|
1163
|
+
${changes}
|
|
1164
|
+
|
|
1165
|
+
After editing SOUL.md, record a brief summary of the retro outcome in your daily log: which proposals were approved and what changed.
|
|
1166
|
+
|
|
1167
|
+
Do NOT modify any other teammate's files. Only edit your own SOUL.md and daily log.`;
|
|
1168
|
+
this.taskQueue.push({ type: "agent", teammate, task: applyPrompt });
|
|
1169
|
+
this.feedLine(concat(tp.muted(" Queued SOUL.md update for "), tp.accent(`@${teammate}`)));
|
|
1170
|
+
this.refreshView();
|
|
1171
|
+
this.kickDrain();
|
|
1172
|
+
}
|
|
1173
|
+
/** Refresh the ChatView app if active. */
|
|
1174
|
+
refreshView() {
|
|
1175
|
+
if (this.app)
|
|
1176
|
+
this.app.refresh();
|
|
1177
|
+
}
|
|
1178
|
+
queueTask(input) {
|
|
1179
|
+
const allNames = this.orchestrator.listTeammates();
|
|
1180
|
+
// Check for @everyone — queue to all teammates except the coding agent
|
|
1181
|
+
const everyoneMatch = input.match(/^@everyone\s+([\s\S]+)$/i);
|
|
1182
|
+
if (everyoneMatch) {
|
|
1183
|
+
const task = everyoneMatch[1];
|
|
1184
|
+
const names = allNames.filter((n) => n !== this.adapterName);
|
|
1185
|
+
for (const teammate of names) {
|
|
1186
|
+
this.taskQueue.push({ type: "agent", teammate, task });
|
|
1187
|
+
}
|
|
1188
|
+
const bg = this._userBg;
|
|
1189
|
+
const t = theme();
|
|
1190
|
+
this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(names.map((n) => `@${n}`).join(", "))));
|
|
1191
|
+
this.feedLine();
|
|
1192
|
+
this.refreshView();
|
|
1193
|
+
this.kickDrain();
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
// Collect all @mentioned teammates anywhere in the input
|
|
1197
|
+
const mentionRegex = /@(\S+)/g;
|
|
1198
|
+
let m;
|
|
1199
|
+
const mentioned = [];
|
|
1200
|
+
while ((m = mentionRegex.exec(input)) !== null) {
|
|
1201
|
+
const name = m[1];
|
|
1202
|
+
if (allNames.includes(name) && !mentioned.includes(name)) {
|
|
1203
|
+
mentioned.push(name);
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
if (mentioned.length > 0) {
|
|
1207
|
+
// Queue a copy of the full message to every mentioned teammate
|
|
1208
|
+
for (const teammate of mentioned) {
|
|
1209
|
+
this.taskQueue.push({ type: "agent", teammate, task: input });
|
|
1210
|
+
}
|
|
1211
|
+
const bg = this._userBg;
|
|
1212
|
+
const t = theme();
|
|
1213
|
+
this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(mentioned.map((n) => `@${n}`).join(", "))));
|
|
1214
|
+
this.feedLine();
|
|
1215
|
+
this.refreshView();
|
|
1216
|
+
this.kickDrain();
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
// No mentions — auto-route: resolve teammate synchronously if possible, else use default
|
|
1220
|
+
let match = this.orchestrator.route(input);
|
|
1221
|
+
if (!match) {
|
|
1222
|
+
// Fall back to adapter name — avoid blocking for agent routing
|
|
1223
|
+
match = this.adapterName;
|
|
1224
|
+
}
|
|
1225
|
+
{
|
|
1226
|
+
const bg = this._userBg;
|
|
1227
|
+
const t = theme();
|
|
1228
|
+
this.feedUserLine(concat(pen.fg(t.textMuted).bg(bg)(" → "), pen.fg(t.accent).bg(bg)(`@${match}`)));
|
|
1229
|
+
}
|
|
1230
|
+
this.feedLine();
|
|
1231
|
+
this.refreshView();
|
|
1232
|
+
this.taskQueue.push({ type: "agent", teammate: match, task: input });
|
|
1233
|
+
this.kickDrain();
|
|
1234
|
+
}
|
|
1235
|
+
/** Start draining per-agent queues in parallel. Each agent gets its own drain loop. */
|
|
1236
|
+
kickDrain() {
|
|
1237
|
+
// Find agents that have queued tasks but no active drain
|
|
1238
|
+
const agentsWithWork = new Set();
|
|
1239
|
+
for (const entry of this.taskQueue) {
|
|
1240
|
+
agentsWithWork.add(entry.teammate);
|
|
1241
|
+
}
|
|
1242
|
+
for (const agent of agentsWithWork) {
|
|
1243
|
+
if (!this.agentDrainLocks.has(agent)) {
|
|
1244
|
+
const lock = this.drainAgentQueue(agent).finally(() => {
|
|
1245
|
+
this.agentDrainLocks.delete(agent);
|
|
1246
|
+
});
|
|
1247
|
+
this.agentDrainLocks.set(agent, lock);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
// ─── Onboarding ───────────────────────────────────────────────────
|
|
1252
|
+
/**
|
|
1253
|
+
* Interactive prompt when no .teammates/ directory is found.
|
|
1254
|
+
* Returns the new .teammates/ path, or null if user chose to exit.
|
|
1255
|
+
*/
|
|
1256
|
+
async promptOnboarding(adapter) {
|
|
1257
|
+
const cwd = process.cwd();
|
|
1258
|
+
const teammatesDir = join(cwd, ".teammates");
|
|
1259
|
+
const termWidth = process.stdout.columns || 100;
|
|
1260
|
+
console.log();
|
|
1261
|
+
this.printLogo([
|
|
1262
|
+
chalk.bold("Teammates") + chalk.gray(` v${PKG_VERSION}`),
|
|
1263
|
+
chalk.yellow("No .teammates/ directory found"),
|
|
1264
|
+
chalk.gray(cwd),
|
|
1265
|
+
]);
|
|
1266
|
+
console.log();
|
|
1267
|
+
console.log(chalk.gray("─".repeat(termWidth)));
|
|
1268
|
+
console.log();
|
|
1269
|
+
console.log(chalk.white(" Set up teammates for this project?\n"));
|
|
1270
|
+
console.log(chalk.cyan(" 1") +
|
|
1271
|
+
chalk.gray(") ") +
|
|
1272
|
+
chalk.white("New team") +
|
|
1273
|
+
chalk.gray(" — analyze this codebase and create teammates from scratch"));
|
|
1274
|
+
console.log(chalk.cyan(" 2") +
|
|
1275
|
+
chalk.gray(") ") +
|
|
1276
|
+
chalk.white("Import team") +
|
|
1277
|
+
chalk.gray(" — copy teammates from another project"));
|
|
1278
|
+
console.log(chalk.cyan(" 3") +
|
|
1279
|
+
chalk.gray(") ") +
|
|
1280
|
+
chalk.white("Solo mode") +
|
|
1281
|
+
chalk.gray(` — use ${this.adapterName} without teammates`));
|
|
1282
|
+
console.log(chalk.cyan(" 4") + chalk.gray(") ") + chalk.white("Exit"));
|
|
1283
|
+
console.log();
|
|
1284
|
+
const choice = await this.askChoice("Pick an option (1/2/3/4): ", [
|
|
1285
|
+
"1",
|
|
1286
|
+
"2",
|
|
1287
|
+
"3",
|
|
1288
|
+
"4",
|
|
1289
|
+
]);
|
|
1290
|
+
if (choice === "4") {
|
|
1291
|
+
console.log(chalk.gray(" Goodbye."));
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
if (choice === "3") {
|
|
1295
|
+
await mkdir(teammatesDir, { recursive: true });
|
|
1296
|
+
console.log();
|
|
1297
|
+
console.log(chalk.green(" ✔") + chalk.gray(` Created ${teammatesDir}`));
|
|
1298
|
+
console.log(chalk.gray(` Running in solo mode — all tasks go to ${this.adapterName}.`));
|
|
1299
|
+
console.log(chalk.gray(" Run /init later to set up teammates."));
|
|
1300
|
+
console.log();
|
|
1301
|
+
return teammatesDir;
|
|
1302
|
+
}
|
|
1303
|
+
if (choice === "2") {
|
|
1304
|
+
// Import from another project
|
|
1305
|
+
await mkdir(teammatesDir, { recursive: true });
|
|
1306
|
+
await this.runImport(cwd);
|
|
1307
|
+
return teammatesDir;
|
|
1308
|
+
}
|
|
1309
|
+
// choice === "1": Run onboarding via the agent
|
|
1310
|
+
await mkdir(teammatesDir, { recursive: true });
|
|
1311
|
+
await this.runOnboardingAgent(adapter, cwd);
|
|
1312
|
+
return teammatesDir;
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* Run the onboarding agent to analyze the codebase and create teammates.
|
|
1316
|
+
* Used by both promptOnboarding (pre-orchestrator) and cmdInit (post-orchestrator).
|
|
1317
|
+
*/
|
|
1318
|
+
async runOnboardingAgent(adapter, projectDir) {
|
|
1319
|
+
console.log();
|
|
1320
|
+
console.log(chalk.blue(" Starting onboarding...") +
|
|
1321
|
+
chalk.gray(` ${this.adapterName} will analyze your codebase and create .teammates/`));
|
|
1322
|
+
console.log();
|
|
1323
|
+
// Copy framework files from bundled template
|
|
1324
|
+
const teammatesDir = join(projectDir, ".teammates");
|
|
1325
|
+
const copied = await copyTemplateFiles(teammatesDir);
|
|
1326
|
+
if (copied.length > 0) {
|
|
1327
|
+
console.log(chalk.green(" ✔") +
|
|
1328
|
+
chalk.gray(` Copied template files: ${copied.join(", ")}`));
|
|
1329
|
+
console.log();
|
|
1330
|
+
}
|
|
1331
|
+
const onboardingPrompt = await getOnboardingPrompt(projectDir);
|
|
1332
|
+
const tempConfig = {
|
|
1333
|
+
name: this.adapterName,
|
|
1334
|
+
role: "Onboarding agent",
|
|
1335
|
+
soul: "",
|
|
1336
|
+
wisdom: "",
|
|
1337
|
+
dailyLogs: [],
|
|
1338
|
+
weeklyLogs: [],
|
|
1339
|
+
ownership: { primary: [], secondary: [] },
|
|
1340
|
+
routingKeywords: [],
|
|
1341
|
+
};
|
|
1342
|
+
const sessionId = await adapter.startSession(tempConfig);
|
|
1343
|
+
const spinner = ora({
|
|
1344
|
+
text: chalk.blue(this.adapterName) +
|
|
1345
|
+
chalk.gray(" is analyzing your codebase..."),
|
|
1346
|
+
spinner: "dots",
|
|
1347
|
+
}).start();
|
|
1348
|
+
try {
|
|
1349
|
+
const result = await adapter.executeTask(sessionId, tempConfig, onboardingPrompt);
|
|
1350
|
+
spinner.stop();
|
|
1351
|
+
this.printAgentOutput(result.rawOutput);
|
|
1352
|
+
if (result.success) {
|
|
1353
|
+
console.log(chalk.green(" ✔ Onboarding complete!"));
|
|
1354
|
+
}
|
|
1355
|
+
else {
|
|
1356
|
+
console.log(chalk.yellow(` ⚠ Onboarding finished with issues: ${result.summary}`));
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
catch (err) {
|
|
1360
|
+
spinner.fail(chalk.red(`Onboarding failed: ${err.message}`));
|
|
1361
|
+
}
|
|
1362
|
+
if (adapter.destroySession) {
|
|
1363
|
+
await adapter.destroySession(sessionId);
|
|
1364
|
+
}
|
|
1365
|
+
// Verify .teammates/ now has content
|
|
1366
|
+
try {
|
|
1367
|
+
const entries = await readdir(teammatesDir);
|
|
1368
|
+
if (!entries.some((e) => !e.startsWith("."))) {
|
|
1369
|
+
console.log(chalk.yellow(" ⚠ .teammates/ was created but appears empty."));
|
|
1370
|
+
console.log(chalk.gray(" You may need to run the onboarding agent again or set up manually."));
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
catch {
|
|
1374
|
+
/* dir might not exist if onboarding failed badly */
|
|
1375
|
+
}
|
|
1376
|
+
console.log();
|
|
1377
|
+
}
|
|
1378
|
+
/**
|
|
1379
|
+
* Import teammates from another project's .teammates/ directory.
|
|
1380
|
+
* Prompts for a path, copies teammate folders + framework files,
|
|
1381
|
+
* then optionally runs the agent to adapt ownership for this codebase.
|
|
1382
|
+
*/
|
|
1383
|
+
async runImport(projectDir) {
|
|
1384
|
+
console.log();
|
|
1385
|
+
console.log(chalk.white(" Enter the path to another project") +
|
|
1386
|
+
chalk.gray(" (the project root or its .teammates/ directory):"));
|
|
1387
|
+
console.log();
|
|
1388
|
+
const rawPath = await this.askInput("Path: ");
|
|
1389
|
+
if (!rawPath) {
|
|
1390
|
+
console.log(chalk.yellow(" No path provided. Aborting import."));
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
// Resolve the source — accept either project root or .teammates/ directly
|
|
1394
|
+
const resolved = resolve(rawPath);
|
|
1395
|
+
let sourceDir;
|
|
1396
|
+
try {
|
|
1397
|
+
const s = await stat(join(resolved, ".teammates"));
|
|
1398
|
+
if (s.isDirectory()) {
|
|
1399
|
+
sourceDir = join(resolved, ".teammates");
|
|
1400
|
+
}
|
|
1401
|
+
else {
|
|
1402
|
+
sourceDir = resolved;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
catch {
|
|
1406
|
+
sourceDir = resolved;
|
|
1407
|
+
}
|
|
1408
|
+
const teammatesDir = join(projectDir, ".teammates");
|
|
1409
|
+
console.log();
|
|
1410
|
+
try {
|
|
1411
|
+
const { teammates, files } = await importTeammates(sourceDir, teammatesDir);
|
|
1412
|
+
if (teammates.length === 0) {
|
|
1413
|
+
console.log(chalk.yellow(" No teammates found at ") + chalk.white(sourceDir));
|
|
1414
|
+
console.log(chalk.gray(" The directory should contain teammate folders (each with a SOUL.md)."));
|
|
1415
|
+
return;
|
|
1416
|
+
}
|
|
1417
|
+
console.log(chalk.green(" ✔") +
|
|
1418
|
+
chalk.white(` Imported ${teammates.length} teammate${teammates.length > 1 ? "s" : ""}: `) +
|
|
1419
|
+
chalk.cyan(teammates.join(", ")));
|
|
1420
|
+
console.log(chalk.gray(` (${files.length} files copied)`));
|
|
1421
|
+
console.log();
|
|
1422
|
+
// Ask if user wants the agent to adapt teammates to this codebase
|
|
1423
|
+
console.log(chalk.white(" Adapt teammates to this codebase?"));
|
|
1424
|
+
console.log(chalk.gray(" The agent will update ownership patterns, file paths, and boundaries."));
|
|
1425
|
+
console.log(chalk.gray(" You can also do this later with /init."));
|
|
1426
|
+
console.log();
|
|
1427
|
+
const adapt = await this.askChoice("Adapt now? (y/n): ", ["y", "n"]);
|
|
1428
|
+
if (adapt === "y") {
|
|
1429
|
+
await this.runAdaptationAgent(this.adapter, projectDir, teammates);
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
console.log(chalk.gray(" Skipped adaptation. Run /init to adapt later."));
|
|
1433
|
+
}
|
|
1434
|
+
}
|
|
1435
|
+
catch (err) {
|
|
1436
|
+
console.log(chalk.red(` Import failed: ${err.message}`));
|
|
1437
|
+
}
|
|
1438
|
+
console.log();
|
|
1439
|
+
}
|
|
1440
|
+
/**
|
|
1441
|
+
* Run the agent to adapt imported teammates' ownership/boundaries
|
|
1442
|
+
* to the current codebase. Queues one task per teammate so the user
|
|
1443
|
+
* can review and approve each adaptation individually.
|
|
1444
|
+
*/
|
|
1445
|
+
async runAdaptationAgent(adapter, projectDir, teammateNames) {
|
|
1446
|
+
const teammatesDir = join(projectDir, ".teammates");
|
|
1447
|
+
console.log();
|
|
1448
|
+
console.log(chalk.blue(" Queuing adaptation tasks...") +
|
|
1449
|
+
chalk.gray(` ${this.adapterName} will adapt each teammate individually`));
|
|
1450
|
+
console.log();
|
|
1451
|
+
for (const name of teammateNames) {
|
|
1452
|
+
const prompt = await buildAdaptationPrompt(teammatesDir, name);
|
|
1453
|
+
const tempConfig = {
|
|
1454
|
+
name: this.adapterName,
|
|
1455
|
+
role: "Adaptation agent",
|
|
1456
|
+
soul: "",
|
|
1457
|
+
wisdom: "",
|
|
1458
|
+
dailyLogs: [],
|
|
1459
|
+
weeklyLogs: [],
|
|
1460
|
+
ownership: { primary: [], secondary: [] },
|
|
1461
|
+
routingKeywords: [],
|
|
1462
|
+
};
|
|
1463
|
+
const sessionId = await adapter.startSession(tempConfig);
|
|
1464
|
+
const spinner = ora({
|
|
1465
|
+
text: chalk.blue(this.adapterName) +
|
|
1466
|
+
chalk.gray(` is adapting @${name} to this codebase...`),
|
|
1467
|
+
spinner: "dots",
|
|
1468
|
+
}).start();
|
|
1469
|
+
try {
|
|
1470
|
+
const result = await adapter.executeTask(sessionId, tempConfig, prompt);
|
|
1471
|
+
spinner.stop();
|
|
1472
|
+
this.printAgentOutput(result.rawOutput);
|
|
1473
|
+
if (result.success) {
|
|
1474
|
+
console.log(chalk.green(` ✔ @${name} adaptation complete!`));
|
|
1475
|
+
}
|
|
1476
|
+
else {
|
|
1477
|
+
console.log(chalk.yellow(` ⚠ @${name} adaptation finished with issues: ${result.summary}`));
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
catch (err) {
|
|
1481
|
+
spinner.fail(chalk.red(`@${name} adaptation failed: ${err.message}`));
|
|
1482
|
+
}
|
|
1483
|
+
if (adapter.destroySession) {
|
|
1484
|
+
await adapter.destroySession(sessionId);
|
|
1485
|
+
}
|
|
1486
|
+
console.log();
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
/**
|
|
1490
|
+
* Simple blocking prompt — reads one line from stdin and validates.
|
|
1491
|
+
*/
|
|
1492
|
+
askChoice(prompt, valid) {
|
|
1493
|
+
return new Promise((resolve) => {
|
|
1494
|
+
const rl = createInterface({
|
|
1495
|
+
input: process.stdin,
|
|
1496
|
+
output: process.stdout,
|
|
1497
|
+
});
|
|
1498
|
+
const ask = () => {
|
|
1499
|
+
rl.question(chalk.cyan(" ") + prompt, (answer) => {
|
|
1500
|
+
const trimmed = answer.trim();
|
|
1501
|
+
if (valid.includes(trimmed)) {
|
|
1502
|
+
rl.close();
|
|
1503
|
+
resolve(trimmed);
|
|
1504
|
+
}
|
|
1505
|
+
else {
|
|
1506
|
+
ask();
|
|
1507
|
+
}
|
|
1508
|
+
});
|
|
1509
|
+
};
|
|
1510
|
+
ask();
|
|
1511
|
+
});
|
|
1512
|
+
}
|
|
1513
|
+
askInput(prompt) {
|
|
1514
|
+
return new Promise((resolve) => {
|
|
1515
|
+
const rl = createInterface({
|
|
1516
|
+
input: process.stdin,
|
|
1517
|
+
output: process.stdout,
|
|
1518
|
+
});
|
|
1519
|
+
rl.question(chalk.cyan(" ") + prompt, (answer) => {
|
|
1520
|
+
rl.close();
|
|
1521
|
+
resolve(answer.trim());
|
|
1522
|
+
});
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
/**
|
|
1526
|
+
* Check whether USER.md needs to be created or is still template placeholders.
|
|
1527
|
+
*/
|
|
1528
|
+
needsUserSetup(teammatesDir) {
|
|
1529
|
+
const userMdPath = join(teammatesDir, "USER.md");
|
|
1530
|
+
try {
|
|
1531
|
+
const content = readFileSync(userMdPath, "utf-8");
|
|
1532
|
+
// Template placeholders contain "<your name>" — treat as not set up
|
|
1533
|
+
return !content.trim() || content.includes("<your name>");
|
|
1534
|
+
}
|
|
1535
|
+
catch {
|
|
1536
|
+
// File doesn't exist
|
|
1537
|
+
return true;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
/**
|
|
1541
|
+
* Run the user interview inside the ChatView using the Interview widget.
|
|
1542
|
+
* Hides the normal input prompt until the interview completes.
|
|
1543
|
+
*/
|
|
1544
|
+
startUserInterview(teammatesDir, bannerWidget) {
|
|
1545
|
+
if (!this.chatView)
|
|
1546
|
+
return;
|
|
1547
|
+
const t = theme();
|
|
1548
|
+
const interview = new Interview({
|
|
1549
|
+
title: "Quick intro — helps teammates tailor their work to you.",
|
|
1550
|
+
subtitle: "(press Enter to skip any question)",
|
|
1551
|
+
questions: [
|
|
1552
|
+
{ key: "name", prompt: "Your name" },
|
|
1553
|
+
{
|
|
1554
|
+
key: "role",
|
|
1555
|
+
prompt: "Your role",
|
|
1556
|
+
placeholder: "e.g., senior backend engineer",
|
|
1557
|
+
},
|
|
1558
|
+
{
|
|
1559
|
+
key: "experience",
|
|
1560
|
+
prompt: "Relevant experience",
|
|
1561
|
+
placeholder: "e.g., 10 years Go, new to React",
|
|
1562
|
+
},
|
|
1563
|
+
{
|
|
1564
|
+
key: "preferences",
|
|
1565
|
+
prompt: "How you like to work",
|
|
1566
|
+
placeholder: "e.g., terse responses, explain reasoning",
|
|
1567
|
+
},
|
|
1568
|
+
{
|
|
1569
|
+
key: "context",
|
|
1570
|
+
prompt: "Anything else",
|
|
1571
|
+
placeholder: "e.g., solo dev, working on a rewrite",
|
|
1572
|
+
},
|
|
1573
|
+
],
|
|
1574
|
+
titleStyle: { fg: t.text },
|
|
1575
|
+
subtitleStyle: { fg: t.textDim, italic: true },
|
|
1576
|
+
promptStyle: { fg: t.accent },
|
|
1577
|
+
answeredStyle: { fg: t.textMuted, italic: true },
|
|
1578
|
+
inputStyle: { fg: t.text },
|
|
1579
|
+
cursorStyle: { fg: t.cursorFg, bg: t.cursorBg },
|
|
1580
|
+
placeholderStyle: { fg: t.textDim, italic: true },
|
|
1581
|
+
});
|
|
1582
|
+
this.chatView.setInputOverride(interview);
|
|
1583
|
+
if (this.app)
|
|
1584
|
+
this.app.refresh();
|
|
1585
|
+
interview.on("complete", (answers) => {
|
|
1586
|
+
// Write USER.md
|
|
1587
|
+
const userMdPath = join(teammatesDir, "USER.md");
|
|
1588
|
+
const lines = ["# User\n"];
|
|
1589
|
+
lines.push(`- **Name:** ${answers.name || "_not provided_"}`);
|
|
1590
|
+
lines.push(`- **Role:** ${answers.role || "_not provided_"}`);
|
|
1591
|
+
lines.push(`- **Experience:** ${answers.experience || "_not provided_"}`);
|
|
1592
|
+
lines.push(`- **Preferences:** ${answers.preferences || "_not provided_"}`);
|
|
1593
|
+
lines.push(`- **Context:** ${answers.context || "_not provided_"}`);
|
|
1594
|
+
writeFileSync(userMdPath, `${lines.join("\n")}\n`, "utf-8");
|
|
1595
|
+
// Remove override and restore normal input
|
|
1596
|
+
if (this.chatView) {
|
|
1597
|
+
this.chatView.setInputOverride(null);
|
|
1598
|
+
this.chatView.appendStyledToFeed(concat(tp.success(" ✔ "), tp.dim("Saved USER.md — update anytime with /user")));
|
|
1599
|
+
}
|
|
1600
|
+
// Release the banner hold so commands animate in
|
|
1601
|
+
if (bannerWidget)
|
|
1602
|
+
bannerWidget.releaseHold();
|
|
1603
|
+
if (this.app)
|
|
1604
|
+
this.app.refresh();
|
|
1605
|
+
});
|
|
1606
|
+
}
|
|
1607
|
+
// ─── Display helpers ──────────────────────────────────────────────
|
|
1608
|
+
/**
|
|
1609
|
+
* Render the box logo with up to 4 info lines on the right side.
|
|
1610
|
+
*/
|
|
1611
|
+
printLogo(infoLines) {
|
|
1612
|
+
const [top, bot] = buildTitle("teammates");
|
|
1613
|
+
console.log(` ${chalk.cyan(top)}`);
|
|
1614
|
+
console.log(` ${chalk.cyan(bot)}`);
|
|
1615
|
+
if (infoLines.length > 0) {
|
|
1616
|
+
console.log();
|
|
1617
|
+
for (const line of infoLines) {
|
|
1618
|
+
console.log(` ${line}`);
|
|
1619
|
+
}
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
/**
|
|
1623
|
+
* Print agent raw output, stripping the trailing JSON protocol block.
|
|
1624
|
+
*/
|
|
1625
|
+
printAgentOutput(rawOutput) {
|
|
1626
|
+
const raw = rawOutput ?? "";
|
|
1627
|
+
if (!raw)
|
|
1628
|
+
return;
|
|
1629
|
+
const cleaned = raw
|
|
1630
|
+
.replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/, "")
|
|
1631
|
+
.trim();
|
|
1632
|
+
if (cleaned) {
|
|
1633
|
+
this.feedMarkdown(cleaned);
|
|
1634
|
+
}
|
|
1635
|
+
this.feedLine();
|
|
1636
|
+
}
|
|
1637
|
+
// ─── Wordwheel ─────────────────────────────────────────────────────
|
|
1638
|
+
getUniqueCommands() {
|
|
1639
|
+
const seen = new Set();
|
|
1640
|
+
const result = [];
|
|
1641
|
+
for (const [, cmd] of this.commands) {
|
|
1642
|
+
if (seen.has(cmd.name))
|
|
1643
|
+
continue;
|
|
1644
|
+
seen.add(cmd.name);
|
|
1645
|
+
result.push(cmd);
|
|
1646
|
+
}
|
|
1647
|
+
return result;
|
|
1648
|
+
}
|
|
1649
|
+
clearWordwheel() {
|
|
1650
|
+
if (this.chatView) {
|
|
1651
|
+
this.chatView.hideDropdown();
|
|
1652
|
+
// Don't refreshView here — caller will either showDropdown + refresh,
|
|
1653
|
+
// or the next App render pass will pick up the cleared state.
|
|
1654
|
+
}
|
|
1655
|
+
else {
|
|
1656
|
+
this.input.clearDropdown();
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
writeWordwheel(lines) {
|
|
1660
|
+
if (this.chatView) {
|
|
1661
|
+
// Lines are pre-formatted for PromptInput — convert to DropdownItems
|
|
1662
|
+
// This path is used for static usage hints; wordwheel items use showDropdown directly
|
|
1663
|
+
this.chatView.showDropdown(lines.map((l) => ({
|
|
1664
|
+
label: stripAnsi(l).trim(),
|
|
1665
|
+
description: "",
|
|
1666
|
+
completion: "",
|
|
1667
|
+
})));
|
|
1668
|
+
this.refreshView();
|
|
1669
|
+
}
|
|
1670
|
+
else {
|
|
1671
|
+
this.input.setDropdown(lines);
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
/**
|
|
1675
|
+
* Which argument positions are teammate-name completable per command.
|
|
1676
|
+
* Key = command name, value = set of 0-based arg positions that take a teammate.
|
|
1677
|
+
*/
|
|
1678
|
+
static TEAMMATE_ARG_POSITIONS = {
|
|
1679
|
+
assign: new Set([0]),
|
|
1680
|
+
handoff: new Set([0, 1]),
|
|
1681
|
+
compact: new Set([0]),
|
|
1682
|
+
debug: new Set([0]),
|
|
1683
|
+
retro: new Set([0]),
|
|
1684
|
+
};
|
|
341
1685
|
/** Build param-completion items for the current line, if any. */
|
|
342
1686
|
getParamItems(cmdName, argsBefore, partial) {
|
|
343
1687
|
// Service-name completions for /install
|
|
@@ -348,70 +1692,117 @@ class TeammatesREPL {
|
|
|
348
1692
|
.map(([name, svc]) => ({
|
|
349
1693
|
label: name,
|
|
350
1694
|
description: svc.description,
|
|
351
|
-
completion:
|
|
1695
|
+
completion: `/install ${name} `,
|
|
352
1696
|
}));
|
|
353
1697
|
}
|
|
354
1698
|
const positions = TeammatesREPL.TEAMMATE_ARG_POSITIONS[cmdName];
|
|
355
1699
|
if (!positions)
|
|
356
1700
|
return [];
|
|
357
1701
|
// Count how many complete args precede the current partial
|
|
358
|
-
const completedArgs = argsBefore.trim()
|
|
1702
|
+
const completedArgs = argsBefore.trim()
|
|
1703
|
+
? argsBefore.trim().split(/\s+/).length
|
|
1704
|
+
: 0;
|
|
359
1705
|
if (!positions.has(completedArgs))
|
|
360
1706
|
return [];
|
|
361
1707
|
const teammates = this.orchestrator.listTeammates();
|
|
362
1708
|
const lower = partial.toLowerCase();
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
1709
|
+
const items = [];
|
|
1710
|
+
// Add "everyone" option at the top (only for first arg position)
|
|
1711
|
+
if (completedArgs === 0 && "everyone".startsWith(lower)) {
|
|
1712
|
+
const linePrefix = `/${cmdName} ${argsBefore ? argsBefore : ""}`;
|
|
1713
|
+
items.push({
|
|
1714
|
+
label: "everyone",
|
|
1715
|
+
description: "all teammates",
|
|
1716
|
+
completion: `${linePrefix}everyone `,
|
|
1717
|
+
});
|
|
1718
|
+
}
|
|
1719
|
+
for (const name of teammates) {
|
|
1720
|
+
if (!name.toLowerCase().startsWith(lower))
|
|
1721
|
+
continue;
|
|
366
1722
|
const t = this.orchestrator.getRegistry().get(name);
|
|
367
|
-
const linePrefix =
|
|
368
|
-
|
|
1723
|
+
const linePrefix = `/${cmdName} ${argsBefore ? argsBefore : ""}`;
|
|
1724
|
+
items.push({
|
|
369
1725
|
label: name,
|
|
370
1726
|
description: t?.role ?? "",
|
|
371
|
-
completion: linePrefix + name
|
|
372
|
-
};
|
|
373
|
-
}
|
|
1727
|
+
completion: `${linePrefix + name} `,
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
return items;
|
|
374
1731
|
}
|
|
375
1732
|
/**
|
|
376
|
-
*
|
|
377
|
-
*
|
|
1733
|
+
* Return dim placeholder hint text for the current input value.
|
|
1734
|
+
* e.g. typing "/log" shows " <teammate>", typing "/log b" shows nothing.
|
|
378
1735
|
*/
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const atPos = left.lastIndexOf("@");
|
|
383
|
-
if (atPos < 0)
|
|
1736
|
+
getCommandHint(value) {
|
|
1737
|
+
const trimmed = value.trimStart();
|
|
1738
|
+
if (!trimmed.startsWith("/"))
|
|
384
1739
|
return null;
|
|
385
|
-
//
|
|
386
|
-
|
|
1740
|
+
// Extract command name and what's been typed after it
|
|
1741
|
+
const spaceIdx = trimmed.indexOf(" ");
|
|
1742
|
+
const cmdName = spaceIdx < 0 ? trimmed.slice(1) : trimmed.slice(1, spaceIdx);
|
|
1743
|
+
const cmd = this.commands.get(cmdName);
|
|
1744
|
+
if (!cmd)
|
|
387
1745
|
return null;
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
if (
|
|
1746
|
+
// Extract placeholder tokens from usage (e.g. "/log [teammate]" → ["[teammate]"])
|
|
1747
|
+
const usageParts = cmd.usage.split(/\s+/).slice(1); // drop the "/command" part
|
|
1748
|
+
if (usageParts.length === 0)
|
|
391
1749
|
return null;
|
|
392
|
-
|
|
1750
|
+
// Count how many args the user has typed after the command
|
|
1751
|
+
const afterCmd = spaceIdx < 0 ? "" : trimmed.slice(spaceIdx + 1);
|
|
1752
|
+
const typedArgs = afterCmd
|
|
1753
|
+
.trim()
|
|
1754
|
+
.split(/\s+/)
|
|
1755
|
+
.filter((s) => s.length > 0);
|
|
1756
|
+
// Show remaining placeholders
|
|
1757
|
+
const remaining = usageParts.slice(typedArgs.length);
|
|
1758
|
+
if (remaining.length === 0)
|
|
1759
|
+
return null;
|
|
1760
|
+
// Add a leading space if the value doesn't already end with one
|
|
1761
|
+
const pad = value.endsWith(" ") ? "" : " ";
|
|
1762
|
+
return pad + remaining.join(" ");
|
|
1763
|
+
}
|
|
1764
|
+
/**
|
|
1765
|
+
* Find the @mention token the cursor is currently inside, if any.
|
|
1766
|
+
* Returns { before, partial, atPos } or null.
|
|
1767
|
+
*/
|
|
1768
|
+
findAtMention(line, cursor) {
|
|
1769
|
+
return findAtMention(line, cursor);
|
|
393
1770
|
}
|
|
394
1771
|
/** Build @mention teammate completion items. */
|
|
395
1772
|
getAtMentionItems(line, before, partial, atPos) {
|
|
396
1773
|
const teammates = this.orchestrator.listTeammates();
|
|
397
1774
|
const lower = partial.toLowerCase();
|
|
398
1775
|
const after = line.slice(atPos + 1 + partial.length);
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
1776
|
+
const items = [];
|
|
1777
|
+
// @everyone alias
|
|
1778
|
+
if ("everyone".startsWith(lower)) {
|
|
1779
|
+
items.push({
|
|
1780
|
+
label: "@everyone",
|
|
1781
|
+
description: "Send to all teammates",
|
|
1782
|
+
completion: `${before}@everyone ${after.replace(/^\s+/, "")}`,
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
for (const name of teammates) {
|
|
1786
|
+
if (name.toLowerCase().startsWith(lower)) {
|
|
1787
|
+
const t = this.orchestrator.getRegistry().get(name);
|
|
1788
|
+
items.push({
|
|
1789
|
+
label: `@${name}`,
|
|
1790
|
+
description: t?.role ?? "",
|
|
1791
|
+
completion: `${before}@${name} ${after.replace(/^\s+/, "")}`,
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
return items;
|
|
409
1796
|
}
|
|
410
1797
|
/** Recompute matches and draw the wordwheel. */
|
|
411
1798
|
updateWordwheel() {
|
|
412
1799
|
this.clearWordwheel();
|
|
413
|
-
const line = this.
|
|
414
|
-
|
|
1800
|
+
const line = this.chatView
|
|
1801
|
+
? this.chatView.inputValue
|
|
1802
|
+
: this.input.line;
|
|
1803
|
+
const cursor = this.chatView
|
|
1804
|
+
? this.chatView.inputValue.length
|
|
1805
|
+
: this.input.cursor;
|
|
415
1806
|
// ── @mention anywhere in the line ──────────────────────────────
|
|
416
1807
|
const mention = this.findAtMention(line, cursor);
|
|
417
1808
|
if (mention) {
|
|
@@ -453,12 +1844,9 @@ class TeammatesREPL {
|
|
|
453
1844
|
this.renderItems();
|
|
454
1845
|
}
|
|
455
1846
|
else {
|
|
456
|
-
// No param completions —
|
|
1847
|
+
// No param completions — hide dropdown
|
|
1848
|
+
this.wordwheelItems = [];
|
|
457
1849
|
this.wordwheelIndex = -1;
|
|
458
|
-
this.writeWordwheel([
|
|
459
|
-
` ${chalk.cyan(cmd.usage)}`,
|
|
460
|
-
` ${chalk.gray(cmd.description)}`,
|
|
461
|
-
]);
|
|
462
1850
|
}
|
|
463
1851
|
return;
|
|
464
1852
|
}
|
|
@@ -467,11 +1855,14 @@ class TeammatesREPL {
|
|
|
467
1855
|
this.wordwheelItems = this.getUniqueCommands()
|
|
468
1856
|
.filter((c) => c.name.startsWith(partial) ||
|
|
469
1857
|
c.aliases.some((a) => a.startsWith(partial)))
|
|
470
|
-
.map((c) =>
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
1858
|
+
.map((c) => {
|
|
1859
|
+
const hasParams = /^\/\S+\s+.+$/.test(c.usage);
|
|
1860
|
+
return {
|
|
1861
|
+
label: `/${c.name}`,
|
|
1862
|
+
description: c.description,
|
|
1863
|
+
completion: hasParams ? `/${c.name} ` : `/${c.name}`,
|
|
1864
|
+
};
|
|
1865
|
+
});
|
|
475
1866
|
if (this.wordwheelItems.length === 0) {
|
|
476
1867
|
this.wordwheelIndex = -1;
|
|
477
1868
|
return;
|
|
@@ -483,14 +1874,30 @@ class TeammatesREPL {
|
|
|
483
1874
|
}
|
|
484
1875
|
/** Render the current wordwheelItems list with selection highlight. */
|
|
485
1876
|
renderItems() {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
if (
|
|
490
|
-
|
|
1877
|
+
if (this.chatView) {
|
|
1878
|
+
this.chatView.showDropdown(this.wordwheelItems);
|
|
1879
|
+
// Sync selection index
|
|
1880
|
+
if (this.wordwheelIndex >= 0) {
|
|
1881
|
+
while (this.chatView.dropdownIndex < this.wordwheelIndex)
|
|
1882
|
+
this.chatView.dropdownDown();
|
|
1883
|
+
while (this.chatView.dropdownIndex > this.wordwheelIndex)
|
|
1884
|
+
this.chatView.dropdownUp();
|
|
491
1885
|
}
|
|
492
|
-
|
|
493
|
-
}
|
|
1886
|
+
this.refreshView();
|
|
1887
|
+
}
|
|
1888
|
+
else {
|
|
1889
|
+
this.writeWordwheel(this.wordwheelItems.map((item, i) => {
|
|
1890
|
+
const prefix = i === this.wordwheelIndex ? chalk.cyan("▸ ") : " ";
|
|
1891
|
+
const label = item.label.padEnd(14);
|
|
1892
|
+
if (i === this.wordwheelIndex) {
|
|
1893
|
+
return (prefix +
|
|
1894
|
+
chalk.cyanBright.bold(label) +
|
|
1895
|
+
" " +
|
|
1896
|
+
chalk.white(item.description));
|
|
1897
|
+
}
|
|
1898
|
+
return `${prefix + chalk.cyan(label)} ${chalk.gray(item.description)}`;
|
|
1899
|
+
}));
|
|
1900
|
+
}
|
|
494
1901
|
}
|
|
495
1902
|
/** Accept the currently highlighted item into the input line. */
|
|
496
1903
|
acceptWordwheelSelection() {
|
|
@@ -498,9 +1905,12 @@ class TeammatesREPL {
|
|
|
498
1905
|
if (!item)
|
|
499
1906
|
return;
|
|
500
1907
|
this.clearWordwheel();
|
|
501
|
-
this.
|
|
502
|
-
|
|
503
|
-
|
|
1908
|
+
if (this.chatView) {
|
|
1909
|
+
this.chatView.inputValue = item.completion;
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
this.input.setLine(item.completion);
|
|
1913
|
+
}
|
|
504
1914
|
this.wordwheelItems = [];
|
|
505
1915
|
this.wordwheelIndex = -1;
|
|
506
1916
|
// Re-render for next param or usage hint
|
|
@@ -517,6 +1927,9 @@ class TeammatesREPL {
|
|
|
517
1927
|
if (!teammatesDir)
|
|
518
1928
|
return; // user chose to exit
|
|
519
1929
|
}
|
|
1930
|
+
// Check if USER.md needs setup — we'll run the interview inside the
|
|
1931
|
+
// ChatView after the UI loads (not before).
|
|
1932
|
+
const pendingUserInterview = this.needsUserSetup(teammatesDir);
|
|
520
1933
|
// Init orchestrator
|
|
521
1934
|
this.teammatesDir = teammatesDir;
|
|
522
1935
|
this.orchestrator = new Orchestrator({
|
|
@@ -531,37 +1944,30 @@ class TeammatesREPL {
|
|
|
531
1944
|
name: this.adapterName,
|
|
532
1945
|
role: `General-purpose coding agent (${this.adapterName})`,
|
|
533
1946
|
soul: "",
|
|
534
|
-
|
|
1947
|
+
wisdom: "",
|
|
535
1948
|
dailyLogs: [],
|
|
1949
|
+
weeklyLogs: [],
|
|
536
1950
|
ownership: { primary: [], secondary: [] },
|
|
1951
|
+
routingKeywords: [],
|
|
537
1952
|
});
|
|
538
1953
|
// Add status entry (init() already ran, so we add it manually)
|
|
539
1954
|
this.orchestrator.getAllStatuses().set(this.adapterName, { state: "idle" });
|
|
540
1955
|
// Populate roster on the adapter so prompts include team info
|
|
541
1956
|
if ("roster" in this.adapter) {
|
|
542
1957
|
const registry = this.orchestrator.getRegistry();
|
|
543
|
-
this.adapter.roster = this.orchestrator
|
|
1958
|
+
this.adapter.roster = this.orchestrator
|
|
1959
|
+
.listTeammates()
|
|
1960
|
+
.map((name) => {
|
|
544
1961
|
const t = registry.get(name);
|
|
545
1962
|
return { name: t.name, role: t.role, ownership: t.ownership };
|
|
546
1963
|
});
|
|
547
1964
|
}
|
|
548
|
-
// Detect installed services and tell the adapter
|
|
1965
|
+
// Detect installed services from services.json and tell the adapter
|
|
549
1966
|
if ("services" in this.adapter) {
|
|
550
1967
|
const services = [];
|
|
551
|
-
// Check if any teammate has a .index/ directory (recall is indexed)
|
|
552
1968
|
try {
|
|
553
|
-
const
|
|
554
|
-
|
|
555
|
-
if (!e.isDirectory() || e.name.startsWith("."))
|
|
556
|
-
return false;
|
|
557
|
-
try {
|
|
558
|
-
return statSync(join(this.teammatesDir, e.name, ".index")).isDirectory();
|
|
559
|
-
}
|
|
560
|
-
catch {
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
});
|
|
564
|
-
if (hasIndex) {
|
|
1969
|
+
const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
|
|
1970
|
+
if (svcJson && "recall" in svcJson) {
|
|
565
1971
|
services.push({
|
|
566
1972
|
name: "recall",
|
|
567
1973
|
description: "Local semantic search across teammate memories and daily logs. Use this to find relevant context before starting a task.",
|
|
@@ -569,311 +1975,542 @@ class TeammatesREPL {
|
|
|
569
1975
|
});
|
|
570
1976
|
}
|
|
571
1977
|
}
|
|
572
|
-
catch {
|
|
1978
|
+
catch {
|
|
1979
|
+
/* no services.json or invalid */
|
|
1980
|
+
}
|
|
573
1981
|
this.adapter.services = services;
|
|
574
1982
|
}
|
|
1983
|
+
// Start recall watch mode if recall is installed
|
|
1984
|
+
this.startRecallWatch();
|
|
1985
|
+
// Background maintenance: compact stale dailies + sync recall indexes
|
|
1986
|
+
this.startupMaintenance().catch(() => { });
|
|
575
1987
|
// Register commands
|
|
576
1988
|
this.registerCommands();
|
|
577
|
-
// Create
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
1989
|
+
// Create PromptInput — consolonia-based replacement for readline.
|
|
1990
|
+
// Uses raw stdin + InputProcessor for proper escape/paste/mouse parsing.
|
|
1991
|
+
// Kept as a fallback for pre-onboarding prompts; the main REPL uses ChatView.
|
|
1992
|
+
this.input = new PromptInput({
|
|
1993
|
+
prompt: chalk.gray("> "),
|
|
1994
|
+
borderStyle: (s) => chalk.gray(s),
|
|
1995
|
+
colorize: (value) => {
|
|
1996
|
+
const validNames = new Set([
|
|
1997
|
+
...this.orchestrator.listTeammates(),
|
|
1998
|
+
this.adapterName,
|
|
1999
|
+
]);
|
|
2000
|
+
return value
|
|
2001
|
+
.replace(/@(\w+)/g, (match, name) => validNames.has(name) ? chalk.blue(match) : match)
|
|
2002
|
+
.replace(/^\/\w+/, (m) => chalk.blue(m));
|
|
585
2003
|
},
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
mutableOutput.cursorTo = process.stdout.cursorTo?.bind(process.stdout);
|
|
592
|
-
mutableOutput.clearLine = process.stdout.clearLine?.bind(process.stdout);
|
|
593
|
-
mutableOutput.moveCursor = process.stdout.moveCursor?.bind(process.stdout);
|
|
594
|
-
mutableOutput.getWindowSize = () => [process.stdout.columns ?? 80, process.stdout.rows ?? 24];
|
|
595
|
-
process.stdout.on("resize", () => {
|
|
596
|
-
mutableOutput.columns = process.stdout.columns;
|
|
597
|
-
mutableOutput.rows = process.stdout.rows;
|
|
598
|
-
mutableOutput.emit("resize");
|
|
599
|
-
});
|
|
600
|
-
this.rl = createInterface({
|
|
601
|
-
input: process.stdin,
|
|
602
|
-
output: mutableOutput,
|
|
603
|
-
prompt: chalk.cyan("teammates") + chalk.gray("> "),
|
|
604
|
-
terminal: true,
|
|
605
|
-
});
|
|
606
|
-
this.dropdown = new Dropdown(this.rl);
|
|
607
|
-
// Pre-mute: if stdin delivers a chunk with multiple newlines (paste),
|
|
608
|
-
// mute output immediately BEFORE readline echoes anything.
|
|
609
|
-
process.stdin.prependListener("data", (chunk) => {
|
|
610
|
-
const str = chunk.toString();
|
|
611
|
-
if (str.includes("\n") && str.indexOf("\n") < str.length - 1) {
|
|
612
|
-
// Multiple lines in one chunk — it's a paste, mute now
|
|
613
|
-
outputMuted = true;
|
|
614
|
-
}
|
|
615
|
-
});
|
|
616
|
-
// Intercept all keypress via _ttyWrite so we can capture
|
|
617
|
-
// arrow-down / arrow-up / Tab for wordwheel navigation.
|
|
618
|
-
// Also used for paste prefix detection via timing heuristic.
|
|
619
|
-
let lastKeystrokeTime = 0;
|
|
620
|
-
const origTtyWrite = this.rl._ttyWrite.bind(this.rl);
|
|
621
|
-
this.rl._ttyWrite = (s, key) => {
|
|
622
|
-
// Timing-based paste prefix detection: if >50ms since last keystroke,
|
|
623
|
-
// this is a new input burst. Snapshot rl.line BEFORE readline processes
|
|
624
|
-
// this character — during a paste burst, characters arrive <5ms apart
|
|
625
|
-
// so the snapshot stays at the pre-paste value.
|
|
626
|
-
const now = Date.now();
|
|
627
|
-
if (now - lastKeystrokeTime > 50) {
|
|
628
|
-
prePastePrefix = this.rl.line ?? "";
|
|
629
|
-
}
|
|
630
|
-
lastKeystrokeTime = now;
|
|
631
|
-
const hasWheel = this.wordwheelItems.length > 0;
|
|
632
|
-
if (hasWheel && key) {
|
|
633
|
-
if (key.name === "down") {
|
|
634
|
-
this.wordwheelIndex = Math.min(this.wordwheelIndex + 1, this.wordwheelItems.length - 1);
|
|
635
|
-
this.renderItems(); // calls dropdown.render() → _refreshLine()
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
if (key.name === "up") {
|
|
2004
|
+
hint: (value) => this.getCommandHint(value),
|
|
2005
|
+
onUpDown: (dir) => {
|
|
2006
|
+
if (this.wordwheelItems.length === 0)
|
|
2007
|
+
return false;
|
|
2008
|
+
if (dir === "up") {
|
|
639
2009
|
this.wordwheelIndex = Math.max(this.wordwheelIndex - 1, -1);
|
|
640
|
-
this.renderItems(); // calls dropdown.render() → _refreshLine()
|
|
641
|
-
return;
|
|
642
2010
|
}
|
|
643
|
-
|
|
644
|
-
this.
|
|
645
|
-
return;
|
|
2011
|
+
else {
|
|
2012
|
+
this.wordwheelIndex = Math.min(this.wordwheelIndex + 1, this.wordwheelItems.length - 1);
|
|
646
2013
|
}
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
if (hasWheel && this.wordwheelIndex >= 0) {
|
|
2014
|
+
this.renderItems();
|
|
2015
|
+
return true;
|
|
2016
|
+
},
|
|
2017
|
+
beforeSubmit: (currentValue) => {
|
|
2018
|
+
if (this.wordwheelItems.length > 0 && this.wordwheelIndex >= 0) {
|
|
653
2019
|
const item = this.wordwheelItems[this.wordwheelIndex];
|
|
654
2020
|
if (item) {
|
|
655
|
-
this.
|
|
656
|
-
this.
|
|
2021
|
+
this.clearWordwheel();
|
|
2022
|
+
this.wordwheelItems = [];
|
|
2023
|
+
this.wordwheelIndex = -1;
|
|
2024
|
+
return item.completion;
|
|
657
2025
|
}
|
|
658
2026
|
}
|
|
659
|
-
this.
|
|
2027
|
+
this.clearWordwheel();
|
|
660
2028
|
this.wordwheelItems = [];
|
|
661
2029
|
this.wordwheelIndex = -1;
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
2030
|
+
return currentValue;
|
|
2031
|
+
},
|
|
2032
|
+
});
|
|
2033
|
+
// ── Build animated banner for ChatView ─────────────────────────────
|
|
2034
|
+
const names = this.orchestrator.listTeammates();
|
|
2035
|
+
const reg = this.orchestrator.getRegistry();
|
|
2036
|
+
let hasRecall = false;
|
|
2037
|
+
try {
|
|
2038
|
+
const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
|
|
2039
|
+
hasRecall = !!(svcJson && "recall" in svcJson);
|
|
2040
|
+
}
|
|
2041
|
+
catch {
|
|
2042
|
+
/* no services.json */
|
|
2043
|
+
}
|
|
2044
|
+
const bannerWidget = new AnimatedBanner({
|
|
2045
|
+
adapterName: this.adapterName,
|
|
2046
|
+
teammateCount: names.length,
|
|
2047
|
+
cwd: process.cwd(),
|
|
2048
|
+
recallInstalled: hasRecall,
|
|
2049
|
+
teammates: names.map((name) => {
|
|
2050
|
+
const t = reg.get(name);
|
|
2051
|
+
return { name, role: t?.role ?? "" };
|
|
2052
|
+
}),
|
|
2053
|
+
});
|
|
2054
|
+
// ── Create ChatView and Consolonia App ────────────────────────────
|
|
2055
|
+
const t = theme();
|
|
2056
|
+
this.chatView = new ChatView({
|
|
2057
|
+
bannerWidget,
|
|
2058
|
+
prompt: "> ",
|
|
2059
|
+
promptStyle: { fg: t.prompt },
|
|
2060
|
+
inputStyle: { fg: t.textMuted },
|
|
2061
|
+
cursorStyle: { fg: t.cursorFg, bg: t.cursorBg },
|
|
2062
|
+
placeholder: " @mention or type a task...",
|
|
2063
|
+
placeholderStyle: { fg: t.textDim, italic: true },
|
|
2064
|
+
inputColorize: (value) => {
|
|
2065
|
+
const styles = new Array(value.length).fill(null);
|
|
2066
|
+
const accentStyle = { fg: theme().accent };
|
|
2067
|
+
const dimStyle = { fg: theme().textDim };
|
|
2068
|
+
// Colorize /commands (only at start of input)
|
|
2069
|
+
const cmdPattern = /^\/[\w-]+/;
|
|
2070
|
+
let m = cmdPattern.exec(value);
|
|
2071
|
+
if (m) {
|
|
2072
|
+
for (let i = m.index; i < m.index + m[0].length; i++) {
|
|
2073
|
+
styles[i] = accentStyle;
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
// Colorize @mentions only if they reference a valid teammate or the coding agent
|
|
2077
|
+
const validNames = new Set([
|
|
2078
|
+
...this.orchestrator.listTeammates(),
|
|
2079
|
+
this.adapterName,
|
|
2080
|
+
]);
|
|
2081
|
+
const mentionPattern = /@(\w+)/g;
|
|
2082
|
+
while ((m = mentionPattern.exec(value)) !== null) {
|
|
2083
|
+
if (validNames.has(m[1])) {
|
|
2084
|
+
for (let i = m.index; i < m.index + m[0].length; i++) {
|
|
2085
|
+
styles[i] = accentStyle;
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
// Colorize [placeholder] blocks as dim
|
|
2090
|
+
const placeholders = /\[[^[\]]+\]/g;
|
|
2091
|
+
while ((m = placeholders.exec(value)) !== null) {
|
|
2092
|
+
for (let i = m.index; i < m.index + m[0].length; i++) {
|
|
2093
|
+
styles[i] = dimStyle;
|
|
2094
|
+
}
|
|
2095
|
+
}
|
|
2096
|
+
return styles;
|
|
2097
|
+
},
|
|
2098
|
+
inputDeleteSize: (value, cursor, direction) => {
|
|
2099
|
+
// Delete entire [placeholder] blocks as a unit (paste placeholders, quoted reply, etc.)
|
|
2100
|
+
const placeholder = /\[[^[\]]+\]/g;
|
|
2101
|
+
let m;
|
|
2102
|
+
while ((m = placeholder.exec(value)) !== null) {
|
|
2103
|
+
const start = m.index;
|
|
2104
|
+
const end = start + m[0].length;
|
|
2105
|
+
if (direction === "backward" && cursor > start && cursor <= end) {
|
|
2106
|
+
return cursor - start;
|
|
2107
|
+
}
|
|
2108
|
+
if (direction === "forward" && cursor >= start && cursor < end) {
|
|
2109
|
+
return end - cursor;
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return 1;
|
|
2113
|
+
},
|
|
2114
|
+
inputHint: (value) => this.getCommandHint(value),
|
|
2115
|
+
inputHintStyle: { fg: t.textDim },
|
|
2116
|
+
maxInputHeight: 5,
|
|
2117
|
+
separatorStyle: { fg: t.separator },
|
|
2118
|
+
progressStyle: { fg: t.progress, italic: true },
|
|
2119
|
+
dropdownHighlightStyle: { fg: t.accent },
|
|
2120
|
+
dropdownStyle: { fg: t.textMuted },
|
|
2121
|
+
footer: concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`)),
|
|
2122
|
+
footerStyle: { fg: t.textDim },
|
|
2123
|
+
});
|
|
2124
|
+
this.defaultFooter = concat(tp.accent(" Teammates"), tp.dim(` v${PKG_VERSION}`));
|
|
2125
|
+
// Wire ChatView events for input handling
|
|
2126
|
+
this.chatView.on("submit", (rawLine) => {
|
|
2127
|
+
this.handleSubmit(rawLine).catch((err) => {
|
|
2128
|
+
this.feedLine(tp.error(`Unhandled error: ${err.message}`));
|
|
2129
|
+
this.refreshView();
|
|
2130
|
+
});
|
|
2131
|
+
});
|
|
2132
|
+
this.chatView.on("change", () => {
|
|
2133
|
+
// Clear quoted reply if user backspaced over the placeholder
|
|
2134
|
+
if (this._pendingQuotedReply &&
|
|
2135
|
+
this.chatView &&
|
|
2136
|
+
!this.chatView.inputValue.includes("[quoted reply]")) {
|
|
2137
|
+
this._pendingQuotedReply = null;
|
|
666
2138
|
}
|
|
667
|
-
// Any other key — clear dropdown, let readline handle keystroke,
|
|
668
|
-
// then recompute and render the new dropdown.
|
|
669
|
-
this.dropdown.clear();
|
|
670
2139
|
this.wordwheelItems = [];
|
|
671
2140
|
this.wordwheelIndex = -1;
|
|
672
|
-
origTtyWrite(s, key);
|
|
673
|
-
// origTtyWrite called _refreshLine which cleared old dropdown.
|
|
674
|
-
// Now compute new items and render (calls _refreshLine again with new suffix).
|
|
675
2141
|
this.updateWordwheel();
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
// Strategy: the first `line` event echoes normally. We immediately
|
|
683
|
-
// mute output so subsequent pasted lines are invisible. After 30ms
|
|
684
|
-
// of quiet, we check: if only 1 line arrived it was normal typing
|
|
685
|
-
// (already echoed, good). If multiple lines arrived, we erase the
|
|
686
|
-
// one echoed line and show a placeholder instead.
|
|
687
|
-
let pasteBuffer = [];
|
|
688
|
-
let pasteTimer = null;
|
|
689
|
-
let pasteCount = 0;
|
|
690
|
-
let prePastePrefix = ""; // text user typed before paste started
|
|
691
|
-
const processPaste = async () => {
|
|
692
|
-
pasteTimer = null;
|
|
693
|
-
outputMuted = false;
|
|
694
|
-
const lines = pasteBuffer;
|
|
695
|
-
pasteBuffer = [];
|
|
696
|
-
if (lines.length === 0)
|
|
697
|
-
return;
|
|
698
|
-
if (lines.length > 1) {
|
|
699
|
-
// Multi-line paste — the first line was echoed, the rest were muted.
|
|
700
|
-
// Erase the first echoed line (move up 1, clear).
|
|
701
|
-
process.stdout.write("\x1b[A\x1b[2K");
|
|
702
|
-
pasteCount++;
|
|
703
|
-
const combined = lines.join("\n");
|
|
704
|
-
const sizeKB = Buffer.byteLength(combined, "utf-8") / 1024;
|
|
705
|
-
const tag = `[Pasted text #${pasteCount} +${lines.length} lines, ${sizeKB.toFixed(1)}KB] `;
|
|
706
|
-
// Store the pasted text — expanded when the user presses Enter.
|
|
707
|
-
this.pastedTexts.set(pasteCount, combined);
|
|
708
|
-
// Restore what the user typed before the paste, plus the placeholder.
|
|
709
|
-
const newLine = prePastePrefix + tag;
|
|
710
|
-
prePastePrefix = ""; // reset for next paste
|
|
711
|
-
this.rl.line = newLine;
|
|
712
|
-
this.rl.cursor = newLine.length;
|
|
713
|
-
this.rl.prompt(true);
|
|
714
|
-
return;
|
|
715
|
-
}
|
|
716
|
-
// Expand paste placeholders with actual content
|
|
717
|
-
const rawLine = lines[0];
|
|
718
|
-
const hasPaste = /\[Pasted text #\d+/.test(rawLine);
|
|
719
|
-
let input = rawLine.replace(/\[Pasted text #(\d+) \+\d+ lines, [\d.]+KB\]\s*/g, (_match, num) => {
|
|
720
|
-
const n = parseInt(num, 10);
|
|
721
|
-
const text = this.pastedTexts.get(n);
|
|
722
|
-
if (text) {
|
|
723
|
-
this.pastedTexts.delete(n);
|
|
724
|
-
return text + "\n";
|
|
2142
|
+
// Reset ESC / Ctrl+C pending state on any text change
|
|
2143
|
+
if (this.escPending) {
|
|
2144
|
+
this.escPending = false;
|
|
2145
|
+
if (this.escTimer) {
|
|
2146
|
+
clearTimeout(this.escTimer);
|
|
2147
|
+
this.escTimer = null;
|
|
725
2148
|
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
if (
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
// Show first few lines as preview
|
|
735
|
-
const previewLines = input.split("\n").slice(0, 5);
|
|
736
|
-
for (const l of previewLines) {
|
|
737
|
-
console.log(chalk.gray(` │ `) + l.slice(0, 120));
|
|
2149
|
+
this.chatView.setFooter(this.defaultFooter);
|
|
2150
|
+
this.refreshView();
|
|
2151
|
+
}
|
|
2152
|
+
if (this.ctrlcPending) {
|
|
2153
|
+
this.ctrlcPending = false;
|
|
2154
|
+
if (this.ctrlcTimer) {
|
|
2155
|
+
clearTimeout(this.ctrlcTimer);
|
|
2156
|
+
this.ctrlcTimer = null;
|
|
738
2157
|
}
|
|
739
|
-
|
|
740
|
-
|
|
2158
|
+
this.chatView.setFooter(this.defaultFooter);
|
|
2159
|
+
this.refreshView();
|
|
2160
|
+
}
|
|
2161
|
+
});
|
|
2162
|
+
this.chatView.on("tab", () => {
|
|
2163
|
+
if (this.wordwheelItems.length > 0) {
|
|
2164
|
+
if (this.wordwheelIndex < 0)
|
|
2165
|
+
this.wordwheelIndex = 0;
|
|
2166
|
+
this.acceptWordwheelSelection();
|
|
2167
|
+
}
|
|
2168
|
+
});
|
|
2169
|
+
this.chatView.on("cancel", () => {
|
|
2170
|
+
this.clearWordwheel();
|
|
2171
|
+
this.wordwheelItems = [];
|
|
2172
|
+
this.wordwheelIndex = -1;
|
|
2173
|
+
if (this.escPending) {
|
|
2174
|
+
// Second ESC — clear input and restore footer
|
|
2175
|
+
this.escPending = false;
|
|
2176
|
+
if (this.escTimer) {
|
|
2177
|
+
clearTimeout(this.escTimer);
|
|
2178
|
+
this.escTimer = null;
|
|
741
2179
|
}
|
|
742
|
-
|
|
2180
|
+
this.chatView.inputValue = "";
|
|
2181
|
+
this.chatView.setFooter(this.defaultFooter);
|
|
2182
|
+
this.pastedTexts.clear();
|
|
2183
|
+
this.refreshView();
|
|
743
2184
|
}
|
|
744
|
-
if (
|
|
745
|
-
|
|
2185
|
+
else if (this.chatView.inputValue.length > 0) {
|
|
2186
|
+
// First ESC with text — show hint in footer, auto-expire after 2s
|
|
2187
|
+
this.escPending = true;
|
|
2188
|
+
const termW = process.stdout.columns || 80;
|
|
2189
|
+
const hint = "ESC again to clear";
|
|
2190
|
+
const pad = Math.max(0, termW - hint.length - 1);
|
|
2191
|
+
this.chatView.setFooter(concat(tp.dim(" ".repeat(pad)), tp.muted(hint)));
|
|
2192
|
+
this.refreshView();
|
|
2193
|
+
this.escTimer = setTimeout(() => {
|
|
2194
|
+
this.escTimer = null;
|
|
2195
|
+
if (this.escPending) {
|
|
2196
|
+
this.escPending = false;
|
|
2197
|
+
this.chatView.setFooter(this.defaultFooter);
|
|
2198
|
+
this.refreshView();
|
|
2199
|
+
}
|
|
2200
|
+
}, 2000);
|
|
2201
|
+
}
|
|
2202
|
+
});
|
|
2203
|
+
this.chatView.on("paste", (text) => {
|
|
2204
|
+
this.handlePaste(text);
|
|
2205
|
+
});
|
|
2206
|
+
this.chatView.on("ctrlc", () => {
|
|
2207
|
+
if (this.ctrlcPending) {
|
|
2208
|
+
// Second Ctrl+C — exit
|
|
2209
|
+
this.ctrlcPending = false;
|
|
2210
|
+
if (this.ctrlcTimer) {
|
|
2211
|
+
clearTimeout(this.ctrlcTimer);
|
|
2212
|
+
this.ctrlcTimer = null;
|
|
2213
|
+
}
|
|
2214
|
+
this.chatView.setFooter(this.defaultFooter);
|
|
2215
|
+
this.stopRecallWatch();
|
|
2216
|
+
if (this.app)
|
|
2217
|
+
this.app.stop();
|
|
2218
|
+
this.orchestrator.shutdown().then(() => process.exit(0));
|
|
746
2219
|
return;
|
|
747
2220
|
}
|
|
748
|
-
|
|
749
|
-
|
|
2221
|
+
// First Ctrl+C — show hint in footer, auto-expire after 2s
|
|
2222
|
+
this.ctrlcPending = true;
|
|
2223
|
+
const termW = process.stdout.columns || 80;
|
|
2224
|
+
const hint = "Ctrl+C again to exit";
|
|
2225
|
+
const pad = Math.max(0, termW - hint.length - 1);
|
|
2226
|
+
this.chatView.setFooter(concat(tp.dim(" ".repeat(pad)), tp.muted(hint)));
|
|
2227
|
+
this.refreshView();
|
|
2228
|
+
this.ctrlcTimer = setTimeout(() => {
|
|
2229
|
+
this.ctrlcTimer = null;
|
|
2230
|
+
if (this.ctrlcPending) {
|
|
2231
|
+
this.ctrlcPending = false;
|
|
2232
|
+
this.chatView.setFooter(this.defaultFooter);
|
|
2233
|
+
this.refreshView();
|
|
2234
|
+
}
|
|
2235
|
+
}, 2000);
|
|
2236
|
+
});
|
|
2237
|
+
this.chatView.on("action", (id) => {
|
|
2238
|
+
if (id === "copy") {
|
|
2239
|
+
this.doCopy(this.lastCleanedOutput || undefined);
|
|
2240
|
+
}
|
|
2241
|
+
else if (id.startsWith("retro-approve-") ||
|
|
2242
|
+
id.startsWith("retro-reject-")) {
|
|
2243
|
+
this.handleRetroAction(id);
|
|
2244
|
+
}
|
|
2245
|
+
else if (id.startsWith("approve-") || id.startsWith("reject-")) {
|
|
2246
|
+
this.handleHandoffAction(id);
|
|
2247
|
+
}
|
|
2248
|
+
else if (id.startsWith("reply-")) {
|
|
2249
|
+
const ctx = this._replyContexts.get(id);
|
|
2250
|
+
if (ctx && this.chatView) {
|
|
2251
|
+
this.chatView.inputValue = `@${ctx.teammate} [quoted reply] `;
|
|
2252
|
+
this._pendingQuotedReply = ctx.message;
|
|
2253
|
+
this.refreshView();
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
this.chatView.on("link", (url) => {
|
|
2258
|
+
const cmd = process.platform === "darwin"
|
|
2259
|
+
? "open"
|
|
2260
|
+
: process.platform === "win32"
|
|
2261
|
+
? "start"
|
|
2262
|
+
: "xdg-open";
|
|
2263
|
+
execCb(`${cmd} ${JSON.stringify(url)}`);
|
|
2264
|
+
});
|
|
2265
|
+
this.app = new App({
|
|
2266
|
+
root: this.chatView,
|
|
2267
|
+
alternateScreen: true,
|
|
2268
|
+
mouse: true,
|
|
2269
|
+
});
|
|
2270
|
+
// Run the app — this takes over the terminal.
|
|
2271
|
+
// Start the banner animation after the first frame renders.
|
|
2272
|
+
bannerWidget.onDirty = () => this.app?.refresh();
|
|
2273
|
+
const runPromise = this.app.run();
|
|
2274
|
+
// Hold the banner animation before commands if we need to run the interview
|
|
2275
|
+
if (pendingUserInterview) {
|
|
2276
|
+
bannerWidget.hold();
|
|
2277
|
+
}
|
|
2278
|
+
bannerWidget.start();
|
|
2279
|
+
// Run user interview inside the ChatView if USER.md needs setup
|
|
2280
|
+
if (pendingUserInterview) {
|
|
2281
|
+
this.startUserInterview(teammatesDir, bannerWidget);
|
|
2282
|
+
}
|
|
2283
|
+
await runPromise;
|
|
2284
|
+
}
|
|
2285
|
+
/**
|
|
2286
|
+
* Handle paste events from ChatView.
|
|
2287
|
+
* For multi-line or large pastes, store the text and replace
|
|
2288
|
+
* the input with a compact placeholder that gets expanded on submit.
|
|
2289
|
+
*/
|
|
2290
|
+
/** Image extensions for drag & drop detection. */
|
|
2291
|
+
// IMAGE_EXTS is now imported from ./cli-utils.js
|
|
2292
|
+
handlePaste(text) {
|
|
2293
|
+
if (!this.chatView)
|
|
2294
|
+
return;
|
|
2295
|
+
// Check if the pasted text is a file path to an image (drag & drop)
|
|
2296
|
+
const trimmed = text.trim().replace(/^["']|["']$/g, ""); // strip quotes from drag & drop paths
|
|
2297
|
+
if (this.isImagePath(trimmed)) {
|
|
2298
|
+
const current = this.chatView.inputValue;
|
|
2299
|
+
const clean = text.replace(/[\r\n]/g, "");
|
|
2300
|
+
const idx = current.indexOf(clean);
|
|
2301
|
+
if (idx >= 0) {
|
|
2302
|
+
const fileName = trimmed.split(/[/\\]/).pop() || trimmed;
|
|
2303
|
+
const n = ++this.pasteCounter;
|
|
2304
|
+
this.pastedTexts.set(n, `[Image: source: ${trimmed}]`);
|
|
2305
|
+
const placeholder = `[Image ${fileName}]`;
|
|
2306
|
+
const newVal = current.slice(0, idx) +
|
|
2307
|
+
placeholder +
|
|
2308
|
+
current.slice(idx + clean.length);
|
|
2309
|
+
this.chatView.inputValue = newVal;
|
|
2310
|
+
this.refreshView();
|
|
2311
|
+
}
|
|
2312
|
+
return;
|
|
2313
|
+
}
|
|
2314
|
+
const lines = text.split(/\r?\n/).length;
|
|
2315
|
+
const sizeKB = (text.length / 1024).toFixed(1);
|
|
2316
|
+
// Only use placeholder for multi-line or large pastes
|
|
2317
|
+
if (lines <= 1 && text.length < 200)
|
|
2318
|
+
return;
|
|
2319
|
+
const n = ++this.pasteCounter;
|
|
2320
|
+
this.pastedTexts.set(n, text);
|
|
2321
|
+
// Replace the pasted text in the input with a placeholder.
|
|
2322
|
+
// The paste was already inserted by TextInput, so we need to
|
|
2323
|
+
// remove it and insert the placeholder instead.
|
|
2324
|
+
const current = this.chatView.inputValue;
|
|
2325
|
+
// The pasted text (with newlines stripped) was inserted at the cursor.
|
|
2326
|
+
// Find it and replace with placeholder.
|
|
2327
|
+
const clean = text.replace(/[\r\n]/g, "");
|
|
2328
|
+
const idx = current.indexOf(clean);
|
|
2329
|
+
if (idx >= 0) {
|
|
2330
|
+
const placeholder = `[Pasted text #${n} +${lines} lines, ${sizeKB}KB]`;
|
|
2331
|
+
const newVal = current.slice(0, idx) + placeholder + current.slice(idx + clean.length);
|
|
2332
|
+
this.chatView.inputValue = newVal;
|
|
2333
|
+
}
|
|
2334
|
+
this.refreshView();
|
|
2335
|
+
}
|
|
2336
|
+
/** Check if a string looks like a path to an image file. */
|
|
2337
|
+
isImagePath(text) {
|
|
2338
|
+
return isImagePath(text);
|
|
2339
|
+
}
|
|
2340
|
+
/** Handle line submission from ChatView. */
|
|
2341
|
+
async handleSubmit(rawLine) {
|
|
2342
|
+
this.clearWordwheel();
|
|
2343
|
+
this.wordwheelItems = [];
|
|
2344
|
+
this.wordwheelIndex = -1;
|
|
2345
|
+
// Expand paste placeholders with actual content
|
|
2346
|
+
let input = rawLine.replace(/\[Pasted text #(\d+) \+\d+ lines, [\d.]+KB\]\s*/g, (_match, num) => {
|
|
2347
|
+
const n = parseInt(num, 10);
|
|
2348
|
+
const text = this.pastedTexts.get(n);
|
|
2349
|
+
if (text) {
|
|
2350
|
+
this.pastedTexts.delete(n);
|
|
2351
|
+
return `${text}\n`;
|
|
2352
|
+
}
|
|
2353
|
+
return "";
|
|
2354
|
+
});
|
|
2355
|
+
// Expand [Image filename] placeholders with stored image source paths
|
|
2356
|
+
input = input
|
|
2357
|
+
.replace(/\[Image [^\]]+\]/g, (match) => {
|
|
2358
|
+
// Find the matching pastedText entry by checking stored values
|
|
2359
|
+
for (const [n, stored] of this.pastedTexts) {
|
|
2360
|
+
if (stored.startsWith("[Image: source:")) {
|
|
2361
|
+
this.pastedTexts.delete(n);
|
|
2362
|
+
return stored;
|
|
2363
|
+
}
|
|
750
2364
|
}
|
|
2365
|
+
return match;
|
|
2366
|
+
})
|
|
2367
|
+
.trim();
|
|
2368
|
+
// Expand [quoted reply] placeholder with blockquoted message
|
|
2369
|
+
if (this._pendingQuotedReply && input.includes("[quoted reply]")) {
|
|
2370
|
+
const quoted = this._pendingQuotedReply
|
|
2371
|
+
.split("\n")
|
|
2372
|
+
.map((l) => `> ${l}`)
|
|
2373
|
+
.join("\n");
|
|
2374
|
+
const before = input.slice(0, input.indexOf("[quoted reply]")).trimEnd();
|
|
2375
|
+
const after = input
|
|
2376
|
+
.slice(input.indexOf("[quoted reply]") + "[quoted reply]".length)
|
|
2377
|
+
.trimStart();
|
|
2378
|
+
const parts = [before, quoted];
|
|
2379
|
+
if (after)
|
|
2380
|
+
parts.push(after);
|
|
2381
|
+
input = parts.join("\n");
|
|
2382
|
+
this._pendingQuotedReply = null;
|
|
2383
|
+
}
|
|
2384
|
+
else {
|
|
2385
|
+
this._pendingQuotedReply = null;
|
|
2386
|
+
}
|
|
2387
|
+
if (!input)
|
|
2388
|
+
return;
|
|
2389
|
+
// Handoff actions
|
|
2390
|
+
if (input === "/approve") {
|
|
2391
|
+
this.handleBulkHandoff("Approve all");
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
if (input === "/always-approve") {
|
|
2395
|
+
this.handleBulkHandoff("Always approve");
|
|
2396
|
+
return;
|
|
2397
|
+
}
|
|
2398
|
+
if (input === "/reject") {
|
|
2399
|
+
this.handleBulkHandoff("Reject all");
|
|
2400
|
+
return;
|
|
2401
|
+
}
|
|
2402
|
+
if (input === "/approve-retro") {
|
|
2403
|
+
this.handleBulkRetro("Approve all");
|
|
2404
|
+
return;
|
|
2405
|
+
}
|
|
2406
|
+
if (input === "/reject-retro") {
|
|
2407
|
+
this.handleBulkRetro("Reject all");
|
|
2408
|
+
return;
|
|
2409
|
+
}
|
|
2410
|
+
// Slash commands
|
|
2411
|
+
if (input.startsWith("/")) {
|
|
751
2412
|
this.dispatching = true;
|
|
752
2413
|
try {
|
|
753
2414
|
await this.dispatch(input);
|
|
754
2415
|
}
|
|
755
2416
|
catch (err) {
|
|
756
|
-
|
|
2417
|
+
this.feedLine(tp.error(`Error: ${err.message}`));
|
|
757
2418
|
}
|
|
758
2419
|
finally {
|
|
759
2420
|
this.dispatching = false;
|
|
760
2421
|
}
|
|
761
|
-
this.
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
// pasted lines don't echo to the terminal.
|
|
770
|
-
if (pasteBuffer.length === 1) {
|
|
771
|
-
outputMuted = true;
|
|
772
|
-
}
|
|
773
|
-
if (pasteTimer)
|
|
774
|
-
clearTimeout(pasteTimer);
|
|
775
|
-
pasteTimer = setTimeout(processPaste, 30);
|
|
776
|
-
});
|
|
777
|
-
this.rl.on("close", async () => {
|
|
778
|
-
this.clearWordwheel();
|
|
779
|
-
console.log(chalk.gray("\nShutting down..."));
|
|
780
|
-
await this.orchestrator.shutdown();
|
|
781
|
-
process.exit(0);
|
|
782
|
-
});
|
|
2422
|
+
this.refreshView();
|
|
2423
|
+
return;
|
|
2424
|
+
}
|
|
2425
|
+
// Everything else gets queued
|
|
2426
|
+
this.conversationHistory.push({ role: "user", text: input });
|
|
2427
|
+
this.printUserMessage(input);
|
|
2428
|
+
this.queueTask(input);
|
|
2429
|
+
this.refreshView();
|
|
783
2430
|
}
|
|
784
2431
|
printBanner(teammates) {
|
|
785
2432
|
const registry = this.orchestrator.getRegistry();
|
|
786
2433
|
const termWidth = process.stdout.columns || 100;
|
|
787
|
-
|
|
788
|
-
// Detect recall — check for .index/ inside any teammate folder
|
|
2434
|
+
// Detect recall from services.json
|
|
789
2435
|
let recallInstalled = false;
|
|
790
2436
|
try {
|
|
791
|
-
const
|
|
792
|
-
|
|
793
|
-
if (!entry.isDirectory() || entry.name.startsWith("."))
|
|
794
|
-
continue;
|
|
795
|
-
try {
|
|
796
|
-
const s = statSync(join(this.teammatesDir, entry.name, ".index"));
|
|
797
|
-
if (s.isDirectory()) {
|
|
798
|
-
recallInstalled = true;
|
|
799
|
-
break;
|
|
800
|
-
}
|
|
801
|
-
}
|
|
802
|
-
catch { /* no index for this teammate */ }
|
|
803
|
-
}
|
|
2437
|
+
const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
|
|
2438
|
+
recallInstalled = !!(svcJson && "recall" in svcJson);
|
|
804
2439
|
}
|
|
805
|
-
catch {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
]);
|
|
2440
|
+
catch {
|
|
2441
|
+
/* no services.json or invalid */
|
|
2442
|
+
}
|
|
2443
|
+
this.feedLine();
|
|
2444
|
+
this.feedLine(concat(tp.bold(" Teammates"), tp.muted(` v${PKG_VERSION}`)));
|
|
2445
|
+
this.feedLine(concat(tp.text(` ${this.adapterName}`), tp.muted(` · ${teammates.length} teammate${teammates.length === 1 ? "" : "s"}`)));
|
|
2446
|
+
this.feedLine(` ${process.cwd()}`);
|
|
2447
|
+
this.feedLine(recallInstalled
|
|
2448
|
+
? tp.success(" ● recall installed")
|
|
2449
|
+
: tp.warning(" ○ recall not installed"));
|
|
816
2450
|
// Roster
|
|
817
|
-
|
|
2451
|
+
this.feedLine();
|
|
818
2452
|
for (const name of teammates) {
|
|
819
2453
|
const t = registry.get(name);
|
|
820
2454
|
if (t) {
|
|
821
|
-
|
|
822
|
-
chalk.cyan("●") +
|
|
823
|
-
chalk.cyan(` @${name}`.padEnd(14)) +
|
|
824
|
-
chalk.gray(t.role));
|
|
2455
|
+
this.feedLine(concat(tp.muted(" "), tp.accent(`● @${name.padEnd(14)}`), tp.muted(t.role)));
|
|
825
2456
|
}
|
|
826
2457
|
}
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
// Quick reference — 3 columns
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
2458
|
+
this.feedLine();
|
|
2459
|
+
this.feedLine(tp.muted("─".repeat(termWidth)));
|
|
2460
|
+
// Quick reference — 3 columns (different set for first run vs normal)
|
|
2461
|
+
let col1;
|
|
2462
|
+
let col2;
|
|
2463
|
+
let col3;
|
|
2464
|
+
if (teammates.length === 0) {
|
|
2465
|
+
// First run — no teammates yet
|
|
2466
|
+
col1 = [
|
|
2467
|
+
["/init", "set up teammates"],
|
|
2468
|
+
["/install", "add a service"],
|
|
2469
|
+
];
|
|
2470
|
+
col2 = [
|
|
2471
|
+
["/help", "all commands"],
|
|
2472
|
+
["/exit", "exit session"],
|
|
2473
|
+
];
|
|
2474
|
+
col3 = [
|
|
2475
|
+
["", ""],
|
|
2476
|
+
["", ""],
|
|
2477
|
+
];
|
|
2478
|
+
}
|
|
2479
|
+
else {
|
|
2480
|
+
col1 = [
|
|
2481
|
+
["@mention", "assign to teammate"],
|
|
2482
|
+
["text", "auto-route task"],
|
|
2483
|
+
["[image]", "drag & drop images"],
|
|
2484
|
+
];
|
|
2485
|
+
col2 = [
|
|
2486
|
+
["/status", "teammates & queue"],
|
|
2487
|
+
["/compact", "compact memory"],
|
|
2488
|
+
["/retro", "run retrospective"],
|
|
2489
|
+
];
|
|
2490
|
+
col3 = [
|
|
2491
|
+
[
|
|
2492
|
+
recallInstalled ? "/copy" : "/install",
|
|
2493
|
+
recallInstalled ? "copy session text" : "add a service",
|
|
2494
|
+
],
|
|
2495
|
+
["/help", "all commands"],
|
|
2496
|
+
["/exit", "exit session"],
|
|
2497
|
+
];
|
|
2498
|
+
}
|
|
845
2499
|
for (let i = 0; i < col1.length; i++) {
|
|
846
|
-
|
|
847
|
-
const c2 = chalk.cyan(col2[i][0].padEnd(12)) + chalk.gray(col2[i][1].padEnd(22));
|
|
848
|
-
const c3 = chalk.cyan(col3[i][0].padEnd(12)) + chalk.gray(col3[i][1]);
|
|
849
|
-
console.log(` ${c1}${c2}${c3}`);
|
|
2500
|
+
this.feedLine(concat(tp.accent(` ${col1[i][0]}`.padEnd(12)), tp.muted(col1[i][1].padEnd(22)), tp.accent(col2[i][0].padEnd(12)), tp.muted(col2[i][1].padEnd(22)), tp.accent(col3[i][0].padEnd(12)), tp.muted(col3[i][1])));
|
|
850
2501
|
}
|
|
851
|
-
|
|
852
|
-
|
|
2502
|
+
this.feedLine();
|
|
2503
|
+
this.refreshView();
|
|
853
2504
|
}
|
|
854
2505
|
registerCommands() {
|
|
855
2506
|
const cmds = [
|
|
856
2507
|
{
|
|
857
2508
|
name: "status",
|
|
858
|
-
aliases: ["s"],
|
|
2509
|
+
aliases: ["s", "queue", "qu"],
|
|
859
2510
|
usage: "/status",
|
|
860
|
-
description: "Show
|
|
2511
|
+
description: "Show teammates, active tasks, and queue",
|
|
861
2512
|
run: () => this.cmdStatus(),
|
|
862
2513
|
},
|
|
863
|
-
{
|
|
864
|
-
name: "teammates",
|
|
865
|
-
aliases: ["team", "t"],
|
|
866
|
-
usage: "/teammates",
|
|
867
|
-
description: "List all teammates and their roles",
|
|
868
|
-
run: () => this.cmdTeammates(),
|
|
869
|
-
},
|
|
870
|
-
{
|
|
871
|
-
name: "log",
|
|
872
|
-
aliases: ["l"],
|
|
873
|
-
usage: "/log [teammate]",
|
|
874
|
-
description: "Show the last task result for a teammate",
|
|
875
|
-
run: (args) => this.cmdLog(args),
|
|
876
|
-
},
|
|
877
2514
|
{
|
|
878
2515
|
name: "help",
|
|
879
2516
|
aliases: ["h", "?"],
|
|
@@ -888,26 +2525,19 @@ class TeammatesREPL {
|
|
|
888
2525
|
description: "Show raw agent output from the last task",
|
|
889
2526
|
run: (args) => this.cmdDebug(args),
|
|
890
2527
|
},
|
|
891
|
-
{
|
|
892
|
-
name: "queue",
|
|
893
|
-
aliases: ["qu"],
|
|
894
|
-
usage: "/queue [@teammate] [task]",
|
|
895
|
-
description: "Add to queue, or show queue if no args",
|
|
896
|
-
run: (args) => this.cmdQueue(args),
|
|
897
|
-
},
|
|
898
2528
|
{
|
|
899
2529
|
name: "cancel",
|
|
900
2530
|
aliases: [],
|
|
901
|
-
usage: "/cancel
|
|
2531
|
+
usage: "/cancel [n]",
|
|
902
2532
|
description: "Cancel a queued task by number",
|
|
903
2533
|
run: (args) => this.cmdCancel(args),
|
|
904
2534
|
},
|
|
905
2535
|
{
|
|
906
2536
|
name: "init",
|
|
907
2537
|
aliases: ["onboard", "setup"],
|
|
908
|
-
usage: "/init",
|
|
909
|
-
description: "
|
|
910
|
-
run: () => this.cmdInit(),
|
|
2538
|
+
usage: "/init [from-path]",
|
|
2539
|
+
description: "Set up teammates (or import from another project)",
|
|
2540
|
+
run: (args) => this.cmdInit(args),
|
|
911
2541
|
},
|
|
912
2542
|
{
|
|
913
2543
|
name: "clear",
|
|
@@ -919,17 +2549,62 @@ class TeammatesREPL {
|
|
|
919
2549
|
{
|
|
920
2550
|
name: "install",
|
|
921
2551
|
aliases: [],
|
|
922
|
-
usage: "/install
|
|
2552
|
+
usage: "/install [service]",
|
|
923
2553
|
description: "Install a teammates service (e.g. recall)",
|
|
924
2554
|
run: (args) => this.cmdInstall(args),
|
|
925
2555
|
},
|
|
2556
|
+
{
|
|
2557
|
+
name: "compact",
|
|
2558
|
+
aliases: [],
|
|
2559
|
+
usage: "/compact [teammate]",
|
|
2560
|
+
description: "Compact daily logs into weekly/monthly summaries",
|
|
2561
|
+
run: (args) => this.cmdCompact(args),
|
|
2562
|
+
},
|
|
2563
|
+
{
|
|
2564
|
+
name: "retro",
|
|
2565
|
+
aliases: [],
|
|
2566
|
+
usage: "/retro [teammate]",
|
|
2567
|
+
description: "Run a structured self-retrospective for a teammate",
|
|
2568
|
+
run: (args) => this.cmdRetro(args),
|
|
2569
|
+
},
|
|
2570
|
+
{
|
|
2571
|
+
name: "copy",
|
|
2572
|
+
aliases: ["cp"],
|
|
2573
|
+
usage: "/copy",
|
|
2574
|
+
description: "Copy session text to clipboard",
|
|
2575
|
+
run: () => this.cmdCopy(),
|
|
2576
|
+
},
|
|
2577
|
+
{
|
|
2578
|
+
name: "user",
|
|
2579
|
+
aliases: [],
|
|
2580
|
+
usage: "/user [change]",
|
|
2581
|
+
description: "View or update USER.md",
|
|
2582
|
+
run: (args) => this.cmdUser(args),
|
|
2583
|
+
},
|
|
2584
|
+
{
|
|
2585
|
+
name: "btw",
|
|
2586
|
+
aliases: [],
|
|
2587
|
+
usage: "/btw [question]",
|
|
2588
|
+
description: "Ask a quick side question without interrupting the main conversation",
|
|
2589
|
+
run: (args) => this.cmdBtw(args),
|
|
2590
|
+
},
|
|
2591
|
+
{
|
|
2592
|
+
name: "theme",
|
|
2593
|
+
aliases: [],
|
|
2594
|
+
usage: "/theme",
|
|
2595
|
+
description: "Show current theme colors",
|
|
2596
|
+
run: () => this.cmdTheme(),
|
|
2597
|
+
},
|
|
926
2598
|
{
|
|
927
2599
|
name: "exit",
|
|
928
2600
|
aliases: ["q", "quit"],
|
|
929
2601
|
usage: "/exit",
|
|
930
2602
|
description: "Exit the session",
|
|
931
2603
|
run: async () => {
|
|
932
|
-
|
|
2604
|
+
this.feedLine(tp.muted("Shutting down..."));
|
|
2605
|
+
this.stopRecallWatch();
|
|
2606
|
+
if (this.app)
|
|
2607
|
+
this.app.stop();
|
|
933
2608
|
await this.orchestrator.shutdown();
|
|
934
2609
|
process.exit(0);
|
|
935
2610
|
},
|
|
@@ -943,471 +2618,395 @@ class TeammatesREPL {
|
|
|
943
2618
|
}
|
|
944
2619
|
}
|
|
945
2620
|
async dispatch(input) {
|
|
946
|
-
//
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
const spaceIdx = input.indexOf(" ");
|
|
954
|
-
const cmdName = spaceIdx > 0 ? input.slice(1, spaceIdx) : input.slice(1);
|
|
955
|
-
const cmdArgs = spaceIdx > 0 ? input.slice(spaceIdx + 1).trim() : "";
|
|
956
|
-
const cmd = this.commands.get(cmdName);
|
|
957
|
-
if (cmd) {
|
|
958
|
-
await cmd.run(cmdArgs);
|
|
959
|
-
}
|
|
960
|
-
else {
|
|
961
|
-
console.log(chalk.yellow(`Unknown command: /${cmdName}`));
|
|
962
|
-
console.log(chalk.gray("Type /help for available commands"));
|
|
963
|
-
}
|
|
2621
|
+
// Dispatch only handles slash commands — text input is queued via queueTask()
|
|
2622
|
+
const spaceIdx = input.indexOf(" ");
|
|
2623
|
+
const cmdName = spaceIdx > 0 ? input.slice(1, spaceIdx) : input.slice(1);
|
|
2624
|
+
const cmdArgs = spaceIdx > 0 ? input.slice(spaceIdx + 1).trim() : "";
|
|
2625
|
+
const cmd = this.commands.get(cmdName);
|
|
2626
|
+
if (cmd) {
|
|
2627
|
+
await cmd.run(cmdArgs);
|
|
964
2628
|
}
|
|
965
2629
|
else {
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
if (mentionMatch) {
|
|
969
|
-
const [, teammate, task] = mentionMatch;
|
|
970
|
-
const names = this.orchestrator.listTeammates();
|
|
971
|
-
if (names.includes(teammate)) {
|
|
972
|
-
await this.cmdAssign(`${teammate} ${task}`);
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
}
|
|
976
|
-
// Also handle @mentions inline: strip @names and route to them
|
|
977
|
-
const inlineMention = input.match(/@(\S+)/);
|
|
978
|
-
if (inlineMention) {
|
|
979
|
-
const teammate = inlineMention[1];
|
|
980
|
-
const names = this.orchestrator.listTeammates();
|
|
981
|
-
if (names.includes(teammate)) {
|
|
982
|
-
const task = input.replace(/@\S+\s*/, "").trim();
|
|
983
|
-
if (task) {
|
|
984
|
-
await this.cmdAssign(`${teammate} ${task}`);
|
|
985
|
-
return;
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
// Bare text — auto-route
|
|
990
|
-
await this.cmdRoute(input);
|
|
2630
|
+
this.feedLine(tp.warning(`Unknown command: /${cmdName}`));
|
|
2631
|
+
this.feedLine(tp.muted("Type /help for available commands"));
|
|
991
2632
|
}
|
|
992
2633
|
}
|
|
993
2634
|
// ─── Event handler ───────────────────────────────────────────────
|
|
994
2635
|
handleEvent(event) {
|
|
995
|
-
// When queue is draining in background, never use spinner — it blocks the prompt
|
|
996
|
-
const useSpinner = !this.queueDraining;
|
|
997
2636
|
switch (event.type) {
|
|
998
|
-
case "task_assigned":
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
else if (!this.queueDraining) {
|
|
1007
|
-
console.log(chalk.blue(` ${event.assignment.teammate}`) +
|
|
1008
|
-
chalk.gray(` is working on: ${event.assignment.task.slice(0, 60)}...`));
|
|
1009
|
-
}
|
|
2637
|
+
case "task_assigned": {
|
|
2638
|
+
// Track this task and start the animated status bar
|
|
2639
|
+
const key = event.assignment.teammate;
|
|
2640
|
+
this.activeTasks.set(key, {
|
|
2641
|
+
teammate: event.assignment.teammate,
|
|
2642
|
+
task: event.assignment.task,
|
|
2643
|
+
});
|
|
2644
|
+
this.startStatusAnimation();
|
|
1010
2645
|
break;
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
const cleaned = raw.replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/, "").trim();
|
|
1019
|
-
const sizeKB = cleaned ? Buffer.byteLength(cleaned, "utf-8") / 1024 : 0;
|
|
1020
|
-
console.log();
|
|
1021
|
-
if (sizeKB > 5) {
|
|
1022
|
-
console.log(chalk.gray(" ─".repeat(40)));
|
|
1023
|
-
console.log(chalk.yellow(` ⚠ Response is ${sizeKB.toFixed(1)}KB — use /debug ${event.result.teammate} to view full output`));
|
|
1024
|
-
console.log(chalk.gray(" ─".repeat(40)));
|
|
1025
|
-
}
|
|
1026
|
-
else if (cleaned) {
|
|
1027
|
-
console.log(cleaned);
|
|
1028
|
-
}
|
|
1029
|
-
console.log();
|
|
1030
|
-
console.log(chalk.green(` ✔ ${event.result.teammate}`) +
|
|
1031
|
-
chalk.gray(": ") +
|
|
1032
|
-
event.result.summary);
|
|
2646
|
+
}
|
|
2647
|
+
case "task_completed": {
|
|
2648
|
+
// Remove from active tasks
|
|
2649
|
+
this.activeTasks.delete(event.result.teammate);
|
|
2650
|
+
// Stop animation if no more active tasks
|
|
2651
|
+
if (this.activeTasks.size === 0) {
|
|
2652
|
+
this.stopStatusAnimation();
|
|
1033
2653
|
}
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
2654
|
+
if (!this.chatView)
|
|
2655
|
+
this.input.deactivateAndErase();
|
|
2656
|
+
const raw = event.result.rawOutput ?? "";
|
|
2657
|
+
// Strip protocol artifacts
|
|
2658
|
+
const cleaned = raw
|
|
2659
|
+
.replace(/^TO:\s*\S+\s*\n/im, "")
|
|
2660
|
+
.replace(/^#\s+.+\n*/m, "")
|
|
2661
|
+
.replace(/```handoff\s*\n@\w+\s*\n[\s\S]*?```/g, "")
|
|
2662
|
+
.replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
|
|
2663
|
+
.trim();
|
|
2664
|
+
const sizeKB = cleaned ? Buffer.byteLength(cleaned, "utf-8") / 1024 : 0;
|
|
2665
|
+
// Header: "teammate: subject"
|
|
2666
|
+
const subject = event.result.summary || "Task completed";
|
|
2667
|
+
this.feedLine(concat(tp.accent(`${event.result.teammate}: `), tp.text(subject)));
|
|
2668
|
+
this.lastCleanedOutput = cleaned;
|
|
2669
|
+
if (sizeKB > 5) {
|
|
2670
|
+
const tmpFile = join(tmpdir(), `teammates-${event.result.teammate}-${Date.now()}.md`);
|
|
2671
|
+
writeFileSync(tmpFile, cleaned, "utf-8");
|
|
2672
|
+
this.feedLine(tp.muted(` ${"─".repeat(40)}`));
|
|
2673
|
+
this.feedLine(tp.warning(` ⚠ Response is ${sizeKB.toFixed(1)}KB — saved to temp file:`));
|
|
2674
|
+
this.feedLine(tp.muted(` ${tmpFile}`));
|
|
2675
|
+
this.feedLine(tp.muted(` ${"─".repeat(40)}`));
|
|
1042
2676
|
}
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
case "handoff_completed":
|
|
1046
|
-
// Already handled via task_completed
|
|
1047
|
-
break;
|
|
1048
|
-
case "error":
|
|
1049
|
-
if (this.spinner) {
|
|
1050
|
-
this.spinner.fail(chalk.red(event.teammate) + chalk.gray(": ") + event.error);
|
|
1051
|
-
this.spinner = null;
|
|
2677
|
+
else if (cleaned) {
|
|
2678
|
+
this.feedMarkdown(cleaned);
|
|
1052
2679
|
}
|
|
1053
2680
|
else {
|
|
1054
|
-
|
|
2681
|
+
this.feedLine(tp.muted(" (no response text — the agent may have only performed tool actions)"));
|
|
2682
|
+
this.feedLine(tp.muted(` Use /debug ${event.result.teammate} to view full output`));
|
|
1055
2683
|
}
|
|
2684
|
+
// Render handoffs
|
|
2685
|
+
const handoffs = event.result.handoffs;
|
|
2686
|
+
if (handoffs.length > 0) {
|
|
2687
|
+
this.renderHandoffs(event.result.teammate, handoffs);
|
|
2688
|
+
}
|
|
2689
|
+
// Clickable [reply] [copy] actions after the response
|
|
2690
|
+
if (this.chatView && cleaned) {
|
|
2691
|
+
const t = theme();
|
|
2692
|
+
const teammate = event.result.teammate;
|
|
2693
|
+
const replyId = `reply-${teammate}-${Date.now()}`;
|
|
2694
|
+
this._replyContexts.set(replyId, { teammate, message: cleaned });
|
|
2695
|
+
this.chatView.appendActionList([
|
|
2696
|
+
{
|
|
2697
|
+
id: replyId,
|
|
2698
|
+
normalStyle: this.makeSpan({
|
|
2699
|
+
text: " [reply]",
|
|
2700
|
+
style: { fg: t.textDim },
|
|
2701
|
+
}),
|
|
2702
|
+
hoverStyle: this.makeSpan({
|
|
2703
|
+
text: " [reply]",
|
|
2704
|
+
style: { fg: t.accent },
|
|
2705
|
+
}),
|
|
2706
|
+
},
|
|
2707
|
+
{
|
|
2708
|
+
id: "copy",
|
|
2709
|
+
normalStyle: this.makeSpan({
|
|
2710
|
+
text: " [copy]",
|
|
2711
|
+
style: { fg: t.textDim },
|
|
2712
|
+
}),
|
|
2713
|
+
hoverStyle: this.makeSpan({
|
|
2714
|
+
text: " [copy]",
|
|
2715
|
+
style: { fg: t.accent },
|
|
2716
|
+
}),
|
|
2717
|
+
},
|
|
2718
|
+
]);
|
|
2719
|
+
}
|
|
2720
|
+
this.feedLine();
|
|
2721
|
+
// Auto-detect new teammates added during this task
|
|
2722
|
+
this.refreshTeammates();
|
|
2723
|
+
this.showPrompt();
|
|
1056
2724
|
break;
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
printHandoffDetails(envelope) {
|
|
1060
|
-
console.log(chalk.gray(" ┌─────────────────────────────────────"));
|
|
1061
|
-
console.log(chalk.gray(" │ ") +
|
|
1062
|
-
chalk.white("Task: ") +
|
|
1063
|
-
envelope.task);
|
|
1064
|
-
if (envelope.changedFiles?.length) {
|
|
1065
|
-
console.log(chalk.gray(" │ ") +
|
|
1066
|
-
chalk.white("Files: ") +
|
|
1067
|
-
envelope.changedFiles.join(", "));
|
|
1068
|
-
}
|
|
1069
|
-
if (envelope.acceptanceCriteria?.length) {
|
|
1070
|
-
console.log(chalk.gray(" │ ") + chalk.white("Criteria:"));
|
|
1071
|
-
for (const c of envelope.acceptanceCriteria) {
|
|
1072
|
-
console.log(chalk.gray(" │ ") + chalk.gray("• ") + c);
|
|
1073
|
-
}
|
|
1074
|
-
}
|
|
1075
|
-
if (envelope.openQuestions?.length) {
|
|
1076
|
-
console.log(chalk.gray(" │ ") + chalk.white("Questions:"));
|
|
1077
|
-
for (const q of envelope.openQuestions) {
|
|
1078
|
-
console.log(chalk.gray(" │ ") + chalk.gray("? ") + q);
|
|
1079
|
-
}
|
|
1080
|
-
}
|
|
1081
|
-
console.log(chalk.gray(" └─────────────────────────────────────"));
|
|
1082
|
-
console.log();
|
|
1083
|
-
console.log(chalk.cyan(" 1") + chalk.gray(") Approve"));
|
|
1084
|
-
console.log(chalk.cyan(" 2") + chalk.gray(") Always approve handoffs"));
|
|
1085
|
-
console.log(chalk.cyan(" 3") + chalk.gray(") Reject"));
|
|
1086
|
-
console.log();
|
|
1087
|
-
}
|
|
1088
|
-
/** Handle the numbered handoff menu choice. */
|
|
1089
|
-
async handleHandoffChoice(choice) {
|
|
1090
|
-
const pending = this.orchestrator.getPendingHandoff();
|
|
1091
|
-
if (!pending)
|
|
1092
|
-
return false;
|
|
1093
|
-
switch (choice) {
|
|
1094
|
-
case "1": {
|
|
1095
|
-
this.orchestrator.clearPendingHandoff(pending.from);
|
|
1096
|
-
const result = await this.orchestrator.assign({
|
|
1097
|
-
teammate: pending.to,
|
|
1098
|
-
task: pending.task,
|
|
1099
|
-
handoff: pending,
|
|
1100
|
-
});
|
|
1101
|
-
this.storeResult(result);
|
|
1102
|
-
return true;
|
|
1103
|
-
}
|
|
1104
|
-
case "2": {
|
|
1105
|
-
this.orchestrator.requireApproval = false;
|
|
1106
|
-
this.orchestrator.clearPendingHandoff(pending.from);
|
|
1107
|
-
console.log(chalk.gray(" Auto-approving all future handoffs."));
|
|
1108
|
-
const result = await this.orchestrator.assign({
|
|
1109
|
-
teammate: pending.to,
|
|
1110
|
-
task: pending.task,
|
|
1111
|
-
handoff: pending,
|
|
1112
|
-
});
|
|
1113
|
-
this.storeResult(result);
|
|
1114
|
-
return true;
|
|
1115
|
-
}
|
|
1116
|
-
case "3": {
|
|
1117
|
-
this.orchestrator.clearPendingHandoff(pending.from);
|
|
1118
|
-
console.log(chalk.gray(` Rejected handoff from `) +
|
|
1119
|
-
chalk.bold(pending.from) +
|
|
1120
|
-
chalk.gray(" to ") +
|
|
1121
|
-
chalk.bold(pending.to));
|
|
1122
|
-
return true;
|
|
1123
2725
|
}
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
return;
|
|
1134
|
-
}
|
|
1135
|
-
const [, teammate, task] = parts;
|
|
1136
|
-
// Pause readline so streamed agent output isn't garbled by the prompt
|
|
1137
|
-
const extraContext = this.buildConversationContext();
|
|
1138
|
-
const result = await this.orchestrator.assign({ teammate, task, extraContext: extraContext || undefined });
|
|
1139
|
-
this.storeResult(result);
|
|
1140
|
-
if (result.handoff && this.orchestrator.requireApproval) {
|
|
1141
|
-
// Handoff is pending — user was already prompted
|
|
1142
|
-
}
|
|
1143
|
-
}
|
|
1144
|
-
async cmdRoute(argsStr) {
|
|
1145
|
-
let match = this.orchestrator.route(argsStr);
|
|
1146
|
-
if (!match) {
|
|
1147
|
-
// Keyword routing didn't find a strong match — ask the agent
|
|
1148
|
-
match = await this.orchestrator.agentRoute(argsStr);
|
|
2726
|
+
case "error":
|
|
2727
|
+
this.activeTasks.delete(event.teammate);
|
|
2728
|
+
if (this.activeTasks.size === 0)
|
|
2729
|
+
this.stopStatusAnimation();
|
|
2730
|
+
if (!this.chatView)
|
|
2731
|
+
this.input.deactivateAndErase();
|
|
2732
|
+
this.feedLine(tp.error(` ✖ ${event.teammate}: ${event.error}`));
|
|
2733
|
+
this.showPrompt();
|
|
2734
|
+
break;
|
|
1149
2735
|
}
|
|
1150
|
-
match = match ?? this.adapterName;
|
|
1151
|
-
console.log(chalk.gray(` Routed to: ${chalk.bold(match)}`));
|
|
1152
|
-
const extraContext = this.buildConversationContext();
|
|
1153
|
-
const result = await this.orchestrator.assign({ teammate: match, task: argsStr, extraContext: extraContext || undefined });
|
|
1154
|
-
this.storeResult(result);
|
|
1155
2736
|
}
|
|
1156
2737
|
async cmdStatus() {
|
|
1157
2738
|
const statuses = this.orchestrator.getAllStatuses();
|
|
1158
2739
|
const registry = this.orchestrator.getRegistry();
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
2740
|
+
this.feedLine();
|
|
2741
|
+
this.feedLine(tp.bold(" Status"));
|
|
2742
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
1162
2743
|
for (const [name, status] of statuses) {
|
|
1163
|
-
const teammate = registry.get(name);
|
|
1164
|
-
const stateColor = status.state === "idle"
|
|
1165
|
-
? chalk.gray
|
|
1166
|
-
: status.state === "working"
|
|
1167
|
-
? chalk.blue
|
|
1168
|
-
: chalk.yellow;
|
|
1169
|
-
const stateLabel = stateColor(status.state.padEnd(16));
|
|
1170
|
-
const nameLabel = chalk.bold(name.padEnd(14));
|
|
1171
|
-
let detail = chalk.gray("—");
|
|
1172
|
-
if (status.lastSummary) {
|
|
1173
|
-
const time = status.lastTimestamp ? chalk.gray(` (${relativeTime(status.lastTimestamp)})`) : "";
|
|
1174
|
-
detail = chalk.white(status.lastSummary.slice(0, 50)) + time;
|
|
1175
|
-
}
|
|
1176
|
-
if (status.state === "pending-handoff" && status.pendingHandoff) {
|
|
1177
|
-
detail = chalk.yellow(`→ ${status.pendingHandoff.to}: ${status.pendingHandoff.task.slice(0, 40)}`);
|
|
1178
|
-
}
|
|
1179
|
-
console.log(` ${nameLabel} ${stateLabel} ${detail}`);
|
|
1180
|
-
}
|
|
1181
|
-
console.log();
|
|
1182
|
-
}
|
|
1183
|
-
async cmdTeammates() {
|
|
1184
|
-
const names = this.orchestrator.listTeammates();
|
|
1185
|
-
const registry = this.orchestrator.getRegistry();
|
|
1186
|
-
console.log();
|
|
1187
|
-
for (const name of names) {
|
|
1188
2744
|
const t = registry.get(name);
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
2745
|
+
const active = this.agentActive.get(name);
|
|
2746
|
+
const queued = this.taskQueue.filter((e) => e.teammate === name);
|
|
2747
|
+
// Teammate name + state
|
|
2748
|
+
const stateLabel = active ? "working" : status.state;
|
|
2749
|
+
const stateColor = stateLabel === "working"
|
|
2750
|
+
? tp.info(` (${stateLabel})`)
|
|
2751
|
+
: tp.muted(` (${stateLabel})`);
|
|
2752
|
+
this.feedLine(concat(tp.accent(` @${name}`), stateColor));
|
|
2753
|
+
// Role
|
|
2754
|
+
if (t) {
|
|
2755
|
+
this.feedLine(tp.muted(` ${t.role}`));
|
|
2756
|
+
}
|
|
2757
|
+
// Active task
|
|
2758
|
+
if (active) {
|
|
2759
|
+
const taskText = active.task.length > 60
|
|
2760
|
+
? `${active.task.slice(0, 57)}…`
|
|
2761
|
+
: active.task;
|
|
2762
|
+
this.feedLine(concat(tp.info(" ▸ "), tp.text(taskText)));
|
|
2763
|
+
}
|
|
2764
|
+
// Queued tasks
|
|
2765
|
+
for (let i = 0; i < queued.length; i++) {
|
|
2766
|
+
const taskText = queued[i].task.length > 60
|
|
2767
|
+
? `${queued[i].task.slice(0, 57)}…`
|
|
2768
|
+
: queued[i].task;
|
|
2769
|
+
this.feedLine(concat(tp.muted(` ${i + 1}. `), tp.muted(taskText)));
|
|
2770
|
+
}
|
|
2771
|
+
// Last result
|
|
2772
|
+
if (!active && status.lastSummary) {
|
|
2773
|
+
const time = status.lastTimestamp
|
|
2774
|
+
? ` ${relativeTime(status.lastTimestamp)}`
|
|
2775
|
+
: "";
|
|
2776
|
+
this.feedLine(tp.muted(` last: ${status.lastSummary.slice(0, 50)}${time}`));
|
|
1195
2777
|
}
|
|
2778
|
+
this.feedLine();
|
|
1196
2779
|
}
|
|
1197
|
-
|
|
2780
|
+
this.refreshView();
|
|
1198
2781
|
}
|
|
1199
|
-
async
|
|
1200
|
-
const
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
2782
|
+
async cmdDebug(argsStr) {
|
|
2783
|
+
const arg = argsStr.trim().replace(/^@/, "");
|
|
2784
|
+
// Resolve targets
|
|
2785
|
+
let targets;
|
|
2786
|
+
if (arg === "everyone") {
|
|
2787
|
+
targets = [];
|
|
2788
|
+
for (const [name, result] of this.lastResults) {
|
|
2789
|
+
if (name !== this.adapterName && result.rawOutput) {
|
|
2790
|
+
targets.push({ name, result });
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
if (targets.length === 0) {
|
|
2794
|
+
this.feedLine(tp.muted(" No raw output available from any teammate."));
|
|
2795
|
+
this.refreshView();
|
|
1206
2796
|
return;
|
|
1207
2797
|
}
|
|
1208
|
-
this.printTeammateLog(teammate, status);
|
|
1209
|
-
}
|
|
1210
|
-
else if (this.lastResult) {
|
|
1211
|
-
// Show last result globally
|
|
1212
|
-
const status = this.orchestrator.getStatus(this.lastResult.teammate);
|
|
1213
|
-
if (status)
|
|
1214
|
-
this.printTeammateLog(this.lastResult.teammate, status);
|
|
1215
2798
|
}
|
|
1216
2799
|
else {
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
if (status.lastSummary) {
|
|
1224
|
-
console.log(chalk.white(` Summary: `) + status.lastSummary);
|
|
1225
|
-
}
|
|
1226
|
-
if (status.lastChangedFiles?.length) {
|
|
1227
|
-
console.log(chalk.white(` Changed:`));
|
|
1228
|
-
for (const f of status.lastChangedFiles) {
|
|
1229
|
-
console.log(chalk.gray(` • `) + f);
|
|
2800
|
+
const result = arg ? this.lastResults.get(arg) : this.lastResult;
|
|
2801
|
+
if (!result?.rawOutput) {
|
|
2802
|
+
this.feedLine(tp.muted(" No raw output available." +
|
|
2803
|
+
(arg ? "" : " Try: /debug <teammate>")));
|
|
2804
|
+
this.refreshView();
|
|
2805
|
+
return;
|
|
1230
2806
|
}
|
|
2807
|
+
targets = [{ name: result.teammate, result }];
|
|
1231
2808
|
}
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
2809
|
+
for (const { name, result } of targets) {
|
|
2810
|
+
this.feedLine();
|
|
2811
|
+
this.feedLine(tp.muted(` ── raw output from ${name} ──`));
|
|
2812
|
+
this.feedLine();
|
|
2813
|
+
this.feedMarkdown(result.rawOutput);
|
|
2814
|
+
this.feedLine();
|
|
2815
|
+
this.feedLine(tp.muted(" ── end raw output ──"));
|
|
1237
2816
|
}
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
2817
|
+
// [copy] action for the debug output
|
|
2818
|
+
if (this.chatView) {
|
|
2819
|
+
const t = theme();
|
|
2820
|
+
this.lastCleanedOutput = targets
|
|
2821
|
+
.map((t) => t.result.rawOutput)
|
|
2822
|
+
.join("\n\n");
|
|
2823
|
+
this.chatView.appendActionList([
|
|
2824
|
+
{
|
|
2825
|
+
id: "copy",
|
|
2826
|
+
normalStyle: this.makeSpan({
|
|
2827
|
+
text: " [copy]",
|
|
2828
|
+
style: { fg: t.textDim },
|
|
2829
|
+
}),
|
|
2830
|
+
hoverStyle: this.makeSpan({
|
|
2831
|
+
text: " [copy]",
|
|
2832
|
+
style: { fg: t.accent },
|
|
2833
|
+
}),
|
|
2834
|
+
},
|
|
2835
|
+
]);
|
|
1248
2836
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
console.log();
|
|
1252
|
-
console.log(result.rawOutput);
|
|
1253
|
-
console.log();
|
|
1254
|
-
console.log(chalk.gray(` ── end raw output ──`));
|
|
1255
|
-
console.log();
|
|
2837
|
+
this.feedLine();
|
|
2838
|
+
this.refreshView();
|
|
1256
2839
|
}
|
|
1257
2840
|
async cmdCancel(argsStr) {
|
|
1258
2841
|
const n = parseInt(argsStr.trim(), 10);
|
|
1259
|
-
if (isNaN(n) || n < 1 || n > this.taskQueue.length) {
|
|
2842
|
+
if (Number.isNaN(n) || n < 1 || n > this.taskQueue.length) {
|
|
1260
2843
|
if (this.taskQueue.length === 0) {
|
|
1261
|
-
|
|
2844
|
+
this.feedLine(tp.muted(" Queue is empty."));
|
|
1262
2845
|
}
|
|
1263
2846
|
else {
|
|
1264
|
-
|
|
2847
|
+
this.feedLine(tp.warning(` Usage: /cancel <1-${this.taskQueue.length}>`));
|
|
1265
2848
|
}
|
|
2849
|
+
this.refreshView();
|
|
1266
2850
|
return;
|
|
1267
2851
|
}
|
|
1268
2852
|
const removed = this.taskQueue.splice(n - 1, 1)[0];
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
2853
|
+
this.feedLine(concat(tp.muted(" Cancelled: "), tp.accent(`@${removed.teammate}`), tp.muted(" — "), tp.text(removed.task.slice(0, 60))));
|
|
2854
|
+
this.refreshView();
|
|
2855
|
+
}
|
|
2856
|
+
/** Drain tasks for a single agent — runs in parallel with other agents. */
|
|
2857
|
+
async drainAgentQueue(agent) {
|
|
2858
|
+
while (true) {
|
|
2859
|
+
const idx = this.taskQueue.findIndex((e) => e.teammate === agent);
|
|
2860
|
+
if (idx < 0)
|
|
2861
|
+
break;
|
|
2862
|
+
const entry = this.taskQueue.splice(idx, 1)[0];
|
|
2863
|
+
this.agentActive.set(agent, entry);
|
|
2864
|
+
try {
|
|
2865
|
+
if (entry.type === "compact") {
|
|
2866
|
+
await this.runCompact(entry.teammate);
|
|
2867
|
+
}
|
|
2868
|
+
else {
|
|
2869
|
+
// btw tasks skip conversation context (side question, not part of main thread)
|
|
2870
|
+
const extraContext = entry.type === "btw" ? "" : this.buildConversationContext();
|
|
2871
|
+
const result = await this.orchestrator.assign({
|
|
2872
|
+
teammate: entry.teammate,
|
|
2873
|
+
task: entry.task,
|
|
2874
|
+
extraContext: extraContext || undefined,
|
|
2875
|
+
});
|
|
2876
|
+
// btw results are not stored in conversation history
|
|
2877
|
+
if (entry.type !== "btw") {
|
|
2878
|
+
this.storeResult(result);
|
|
2879
|
+
}
|
|
2880
|
+
if (entry.type === "retro") {
|
|
2881
|
+
this.handleRetroResult(result);
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
1280
2884
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
chalk.white(this.queueActive.task.length > 60 ? this.queueActive.task.slice(0, 57) + "..." : this.queueActive.task) +
|
|
1290
|
-
chalk.blue(" (running)"));
|
|
1291
|
-
}
|
|
1292
|
-
for (let i = 0; i < this.taskQueue.length; i++) {
|
|
1293
|
-
const entry = this.taskQueue[i];
|
|
1294
|
-
console.log(chalk.gray(` ${i + 1}. `) +
|
|
1295
|
-
chalk.cyan(`@${entry.teammate}`) +
|
|
1296
|
-
chalk.gray(" — ") +
|
|
1297
|
-
chalk.white(entry.task.length > 60 ? entry.task.slice(0, 57) + "..." : entry.task));
|
|
1298
|
-
}
|
|
1299
|
-
if (this.taskQueue.length > 0) {
|
|
1300
|
-
console.log(chalk.gray(" /cancel <n> to remove a task"));
|
|
2885
|
+
catch (err) {
|
|
2886
|
+
// Handle spawn failures, network errors, etc. gracefully
|
|
2887
|
+
this.activeTasks.delete(agent);
|
|
2888
|
+
if (this.activeTasks.size === 0)
|
|
2889
|
+
this.stopStatusAnimation();
|
|
2890
|
+
const msg = err?.message ?? String(err);
|
|
2891
|
+
this.feedLine(tp.error(` ✖ @${agent}: ${msg}`));
|
|
2892
|
+
this.refreshView();
|
|
1301
2893
|
}
|
|
1302
|
-
|
|
1303
|
-
return;
|
|
1304
|
-
}
|
|
1305
|
-
// Parse: @teammate task or teammate task
|
|
1306
|
-
const match = argsStr.match(/^@?(\S+)(?:\s+([\s\S]+))?$/);
|
|
1307
|
-
if (!match) {
|
|
1308
|
-
console.log(chalk.yellow(" Usage: /queue @teammate <task...>"));
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
const [, teammate, task] = match;
|
|
1312
|
-
const names = this.orchestrator.listTeammates();
|
|
1313
|
-
if (!names.includes(teammate)) {
|
|
1314
|
-
console.log(chalk.yellow(` Unknown teammate: ${teammate}`));
|
|
1315
|
-
return;
|
|
1316
|
-
}
|
|
1317
|
-
if (!task?.trim()) {
|
|
1318
|
-
console.log(chalk.yellow(` Missing task. Usage: /queue @${teammate} <task...>`));
|
|
1319
|
-
return;
|
|
1320
|
-
}
|
|
1321
|
-
this.taskQueue.push({ teammate, task: task.trim() });
|
|
1322
|
-
console.log();
|
|
1323
|
-
console.log(chalk.gray(" Queued: ") +
|
|
1324
|
-
chalk.cyan(`@${teammate}`) +
|
|
1325
|
-
chalk.gray(" — ") +
|
|
1326
|
-
chalk.white(task.trim().slice(0, 60)) +
|
|
1327
|
-
chalk.gray(` (${this.taskQueue.length} in queue)`));
|
|
1328
|
-
console.log(chalk.blue(` ${teammate}`) +
|
|
1329
|
-
chalk.gray(` is working on: ${task.trim().slice(0, 60)}...`));
|
|
1330
|
-
console.log();
|
|
1331
|
-
// Start draining if not already (mutex-protected)
|
|
1332
|
-
if (!this.drainLock) {
|
|
1333
|
-
this.drainLock = this.drainQueue().finally(() => { this.drainLock = null; });
|
|
2894
|
+
this.agentActive.delete(agent);
|
|
1334
2895
|
}
|
|
1335
2896
|
}
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
2897
|
+
async cmdInit(argsStr) {
|
|
2898
|
+
const cwd = process.cwd();
|
|
2899
|
+
const teammatesDir = join(cwd, ".teammates");
|
|
2900
|
+
await mkdir(teammatesDir, { recursive: true });
|
|
2901
|
+
const fromPath = argsStr.trim();
|
|
2902
|
+
if (fromPath) {
|
|
2903
|
+
// Import mode: /init <path-to-another-project>
|
|
2904
|
+
const resolved = resolve(fromPath);
|
|
2905
|
+
let sourceDir;
|
|
2906
|
+
try {
|
|
2907
|
+
const s = await stat(join(resolved, ".teammates"));
|
|
2908
|
+
if (s.isDirectory()) {
|
|
2909
|
+
sourceDir = join(resolved, ".teammates");
|
|
2910
|
+
}
|
|
2911
|
+
else {
|
|
2912
|
+
sourceDir = resolved;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
catch {
|
|
2916
|
+
sourceDir = resolved;
|
|
2917
|
+
}
|
|
2918
|
+
try {
|
|
2919
|
+
const { teammates, files } = await importTeammates(sourceDir, teammatesDir);
|
|
2920
|
+
if (teammates.length === 0) {
|
|
2921
|
+
this.feedLine(tp.warning(` No teammates found at ${sourceDir}`));
|
|
2922
|
+
this.refreshView();
|
|
2923
|
+
return;
|
|
2924
|
+
}
|
|
2925
|
+
this.feedLine(tp.success(` Imported ${teammates.length} teammate${teammates.length > 1 ? "s" : ""}: ${teammates.join(", ")} (${files.length} files)`));
|
|
2926
|
+
// Queue one adaptation task per teammate
|
|
2927
|
+
this.feedLine(tp.muted(` Queuing ${this.adapterName} to adapt each teammate individually...`));
|
|
2928
|
+
for (const name of teammates) {
|
|
2929
|
+
const prompt = await buildAdaptationPrompt(teammatesDir, name);
|
|
2930
|
+
this.taskQueue.push({
|
|
2931
|
+
type: "agent",
|
|
2932
|
+
teammate: this.adapterName,
|
|
2933
|
+
task: prompt,
|
|
1353
2934
|
});
|
|
1354
|
-
continue;
|
|
1355
2935
|
}
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
teammate: entry.teammate,
|
|
1361
|
-
task: entry.task,
|
|
1362
|
-
extraContext: extraContext || undefined,
|
|
1363
|
-
});
|
|
1364
|
-
this.queueActive = null;
|
|
1365
|
-
this.storeResult(result);
|
|
2936
|
+
this.kickDrain();
|
|
2937
|
+
}
|
|
2938
|
+
catch (err) {
|
|
2939
|
+
this.feedLine(tp.error(` Import failed: ${err.message}`));
|
|
1366
2940
|
}
|
|
1367
|
-
console.log(chalk.green(" ✔ Queue complete."));
|
|
1368
|
-
this.rl.prompt();
|
|
1369
2941
|
}
|
|
1370
|
-
|
|
1371
|
-
|
|
2942
|
+
else {
|
|
2943
|
+
// Normal onboarding
|
|
2944
|
+
await this.runOnboardingAgent(this.adapter, cwd);
|
|
1372
2945
|
}
|
|
1373
|
-
}
|
|
1374
|
-
async cmdInit() {
|
|
1375
|
-
const cwd = process.cwd();
|
|
1376
|
-
await mkdir(join(cwd, ".teammates"), { recursive: true });
|
|
1377
|
-
await this.runOnboardingAgent(this.adapter, cwd);
|
|
1378
2946
|
// Reload the registry to pick up newly created teammates
|
|
1379
|
-
await this.orchestrator.
|
|
1380
|
-
|
|
2947
|
+
const added = await this.orchestrator.refresh();
|
|
2948
|
+
if (added.length > 0) {
|
|
2949
|
+
const registry = this.orchestrator.getRegistry();
|
|
2950
|
+
if ("roster" in this.adapter) {
|
|
2951
|
+
this.adapter.roster = this.orchestrator
|
|
2952
|
+
.listTeammates()
|
|
2953
|
+
.map((name) => {
|
|
2954
|
+
const t = registry.get(name);
|
|
2955
|
+
return { name: t.name, role: t.role, ownership: t.ownership };
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
this.feedLine(tp.muted(" Run /status to see the roster."));
|
|
2960
|
+
this.refreshView();
|
|
1381
2961
|
}
|
|
1382
2962
|
async cmdInstall(argsStr) {
|
|
1383
2963
|
const serviceName = argsStr.trim().toLowerCase();
|
|
1384
2964
|
if (!serviceName) {
|
|
1385
|
-
|
|
2965
|
+
this.feedLine(tp.bold("\n Available services:"));
|
|
1386
2966
|
for (const [name, svc] of Object.entries(SERVICE_REGISTRY)) {
|
|
1387
|
-
|
|
2967
|
+
this.feedLine(concat(tp.accent(name.padEnd(16)), tp.muted(svc.description)));
|
|
1388
2968
|
}
|
|
1389
|
-
|
|
2969
|
+
this.feedLine();
|
|
2970
|
+
this.refreshView();
|
|
1390
2971
|
return;
|
|
1391
2972
|
}
|
|
1392
2973
|
const service = SERVICE_REGISTRY[serviceName];
|
|
1393
2974
|
if (!service) {
|
|
1394
|
-
|
|
1395
|
-
|
|
2975
|
+
this.feedLine(tp.warning(` Unknown service: ${serviceName}`));
|
|
2976
|
+
this.feedLine(tp.muted(` Available: ${Object.keys(SERVICE_REGISTRY).join(", ")}`));
|
|
2977
|
+
this.refreshView();
|
|
1396
2978
|
return;
|
|
1397
2979
|
}
|
|
1398
2980
|
// Install the package globally
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
2981
|
+
if (this.chatView) {
|
|
2982
|
+
this.chatView.setProgress(`Installing ${service.package}...`);
|
|
2983
|
+
this.refreshView();
|
|
2984
|
+
}
|
|
2985
|
+
let installSpinner = null;
|
|
2986
|
+
if (!this.chatView) {
|
|
2987
|
+
installSpinner = ora({
|
|
2988
|
+
text: chalk.blue(serviceName) +
|
|
2989
|
+
chalk.gray(` installing ${service.package}...`),
|
|
2990
|
+
spinner: "dots",
|
|
2991
|
+
}).start();
|
|
2992
|
+
}
|
|
1403
2993
|
try {
|
|
1404
2994
|
await execAsync(`npm install -g ${service.package}`, {
|
|
1405
2995
|
timeout: 5 * 60 * 1000,
|
|
1406
2996
|
});
|
|
1407
|
-
|
|
2997
|
+
if (installSpinner)
|
|
2998
|
+
installSpinner.stop();
|
|
2999
|
+
if (this.chatView)
|
|
3000
|
+
this.chatView.setProgress(null);
|
|
1408
3001
|
}
|
|
1409
3002
|
catch (err) {
|
|
1410
|
-
|
|
3003
|
+
if (installSpinner)
|
|
3004
|
+
installSpinner.fail(chalk.red(`Install failed: ${err.message}`));
|
|
3005
|
+
if (this.chatView) {
|
|
3006
|
+
this.chatView.setProgress(null);
|
|
3007
|
+
this.feedLine(tp.error(` ✖ Install failed: ${err.message}`));
|
|
3008
|
+
this.refreshView();
|
|
3009
|
+
}
|
|
1411
3010
|
return;
|
|
1412
3011
|
}
|
|
1413
3012
|
// Verify the binary works
|
|
@@ -1416,57 +3015,499 @@ class TeammatesREPL {
|
|
|
1416
3015
|
execSync(checkCmdStr, { stdio: "ignore" });
|
|
1417
3016
|
}
|
|
1418
3017
|
catch {
|
|
1419
|
-
|
|
1420
|
-
|
|
3018
|
+
this.feedLine(tp.success(` ✔ ${serviceName} installed`));
|
|
3019
|
+
this.feedLine(tp.warning(` ⚠ Restart your terminal to add ${service.checkCmd[0]} to your PATH, then run /install ${serviceName} again to build the index.`));
|
|
3020
|
+
this.refreshView();
|
|
1421
3021
|
return;
|
|
1422
3022
|
}
|
|
1423
|
-
|
|
3023
|
+
this.feedLine(tp.success(` ✔ ${serviceName} installed successfully`));
|
|
3024
|
+
// Register in services.json
|
|
3025
|
+
const svcPath = join(this.teammatesDir, "services.json");
|
|
3026
|
+
let svcJson = {};
|
|
3027
|
+
try {
|
|
3028
|
+
svcJson = JSON.parse(readFileSync(svcPath, "utf-8"));
|
|
3029
|
+
}
|
|
3030
|
+
catch {
|
|
3031
|
+
/* new file */
|
|
3032
|
+
}
|
|
3033
|
+
if (!(serviceName in svcJson)) {
|
|
3034
|
+
svcJson[serviceName] = {};
|
|
3035
|
+
writeFileSync(svcPath, `${JSON.stringify(svcJson, null, 2)}\n`);
|
|
3036
|
+
this.feedLine(tp.muted(` Registered in services.json`));
|
|
3037
|
+
}
|
|
1424
3038
|
// Build initial index if this service supports it
|
|
1425
3039
|
if (service.indexCmd) {
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
}
|
|
3040
|
+
if (this.chatView) {
|
|
3041
|
+
this.chatView.setProgress(`Building ${serviceName} index...`);
|
|
3042
|
+
this.refreshView();
|
|
3043
|
+
}
|
|
3044
|
+
let idxSpinner = null;
|
|
3045
|
+
if (!this.chatView) {
|
|
3046
|
+
idxSpinner = ora({
|
|
3047
|
+
text: chalk.blue(serviceName) + chalk.gray(` building index...`),
|
|
3048
|
+
spinner: "dots",
|
|
3049
|
+
}).start();
|
|
3050
|
+
}
|
|
1430
3051
|
const indexCmdStr = service.indexCmd.join(" ");
|
|
1431
3052
|
try {
|
|
1432
3053
|
await execAsync(indexCmdStr, {
|
|
1433
3054
|
cwd: resolve(this.teammatesDir, ".."),
|
|
1434
3055
|
timeout: 5 * 60 * 1000,
|
|
1435
3056
|
});
|
|
1436
|
-
|
|
3057
|
+
if (idxSpinner)
|
|
3058
|
+
idxSpinner.succeed(chalk.blue(serviceName) + chalk.gray(" index built"));
|
|
3059
|
+
if (this.chatView) {
|
|
3060
|
+
this.chatView.setProgress(null);
|
|
3061
|
+
this.feedLine(tp.success(` ✔ ${serviceName} index built`));
|
|
3062
|
+
}
|
|
1437
3063
|
}
|
|
1438
3064
|
catch (err) {
|
|
1439
|
-
|
|
3065
|
+
if (idxSpinner)
|
|
3066
|
+
idxSpinner.warn(chalk.yellow(`Index build failed: ${err.message}`));
|
|
3067
|
+
if (this.chatView) {
|
|
3068
|
+
this.chatView.setProgress(null);
|
|
3069
|
+
this.feedLine(tp.warning(` ⚠ Index build failed: ${err.message}`));
|
|
3070
|
+
}
|
|
1440
3071
|
}
|
|
1441
3072
|
}
|
|
1442
3073
|
// Ask the coding agent to wire the service into the project
|
|
1443
3074
|
if (service.wireupTask) {
|
|
1444
|
-
|
|
1445
|
-
|
|
3075
|
+
this.feedLine();
|
|
3076
|
+
this.feedLine(tp.muted(` Wiring up ${serviceName}...`));
|
|
3077
|
+
this.refreshView();
|
|
1446
3078
|
const result = await this.orchestrator.assign({
|
|
1447
3079
|
teammate: this.adapterName,
|
|
1448
3080
|
task: service.wireupTask,
|
|
1449
3081
|
});
|
|
1450
3082
|
this.storeResult(result);
|
|
1451
3083
|
}
|
|
3084
|
+
this.refreshView();
|
|
1452
3085
|
}
|
|
1453
3086
|
async cmdClear() {
|
|
1454
|
-
// Reset all session state
|
|
1455
3087
|
this.conversationHistory.length = 0;
|
|
1456
3088
|
this.lastResult = null;
|
|
1457
3089
|
this.lastResults.clear();
|
|
1458
3090
|
this.taskQueue.length = 0;
|
|
1459
|
-
this.
|
|
3091
|
+
this.agentActive.clear();
|
|
1460
3092
|
this.pastedTexts.clear();
|
|
3093
|
+
this.pendingRetroProposals = [];
|
|
1461
3094
|
await this.orchestrator.reset();
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
3095
|
+
if (this.chatView) {
|
|
3096
|
+
this.chatView.clear();
|
|
3097
|
+
this.refreshView();
|
|
3098
|
+
}
|
|
3099
|
+
else {
|
|
3100
|
+
process.stdout.write(esc.clearScreen + esc.moveTo(0, 0));
|
|
3101
|
+
this.printBanner(this.orchestrator.listTeammates());
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
3104
|
+
/**
|
|
3105
|
+
* Reload the registry from disk. If new teammates appeared,
|
|
3106
|
+
* announce them, update the adapter roster, and refresh statuses.
|
|
3107
|
+
*/
|
|
3108
|
+
refreshTeammates() {
|
|
3109
|
+
this.orchestrator
|
|
3110
|
+
.refresh()
|
|
3111
|
+
.then((added) => {
|
|
3112
|
+
if (added.length === 0)
|
|
3113
|
+
return;
|
|
3114
|
+
const registry = this.orchestrator.getRegistry();
|
|
3115
|
+
// Update adapter roster so prompts include the new teammates
|
|
3116
|
+
if ("roster" in this.adapter) {
|
|
3117
|
+
this.adapter.roster = this.orchestrator
|
|
3118
|
+
.listTeammates()
|
|
3119
|
+
.map((name) => {
|
|
3120
|
+
const t = registry.get(name);
|
|
3121
|
+
return { name: t.name, role: t.role, ownership: t.ownership };
|
|
3122
|
+
});
|
|
3123
|
+
}
|
|
3124
|
+
// Announce
|
|
3125
|
+
for (const name of added) {
|
|
3126
|
+
const config = registry.get(name);
|
|
3127
|
+
const role = config?.role ?? "teammate";
|
|
3128
|
+
this.feedLine(concat(tp.success(` ✦ New teammate joined: `), tp.bold(name), tp.muted(` — ${role}`)));
|
|
3129
|
+
}
|
|
3130
|
+
this.refreshView();
|
|
3131
|
+
})
|
|
3132
|
+
.catch(() => { });
|
|
3133
|
+
}
|
|
3134
|
+
startRecallWatch() {
|
|
3135
|
+
// Only start if recall is installed (check services.json)
|
|
3136
|
+
try {
|
|
3137
|
+
const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
|
|
3138
|
+
if (!svcJson || !("recall" in svcJson))
|
|
3139
|
+
return;
|
|
3140
|
+
}
|
|
3141
|
+
catch {
|
|
3142
|
+
return; // No services.json — recall not installed
|
|
3143
|
+
}
|
|
3144
|
+
try {
|
|
3145
|
+
this.recallWatchProcess = cpSpawn("teammates-recall", ["watch", "--dir", this.teammatesDir, "--json"], {
|
|
3146
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
3147
|
+
detached: false,
|
|
3148
|
+
});
|
|
3149
|
+
this.recallWatchProcess.on("error", () => {
|
|
3150
|
+
// Recall binary not found — silently ignore
|
|
3151
|
+
this.recallWatchProcess = null;
|
|
3152
|
+
});
|
|
3153
|
+
this.recallWatchProcess.on("exit", () => {
|
|
3154
|
+
this.recallWatchProcess = null;
|
|
3155
|
+
});
|
|
3156
|
+
}
|
|
3157
|
+
catch {
|
|
3158
|
+
this.recallWatchProcess = null;
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
3161
|
+
stopRecallWatch() {
|
|
3162
|
+
if (this.recallWatchProcess) {
|
|
3163
|
+
this.recallWatchProcess.kill("SIGTERM");
|
|
3164
|
+
this.recallWatchProcess = null;
|
|
3165
|
+
}
|
|
3166
|
+
}
|
|
3167
|
+
async cmdCompact(argsStr) {
|
|
3168
|
+
const arg = argsStr.trim();
|
|
3169
|
+
const allTeammates = this.orchestrator
|
|
3170
|
+
.listTeammates()
|
|
3171
|
+
.filter((n) => n !== this.adapterName);
|
|
3172
|
+
const names = !arg || arg === "everyone" ? allTeammates : [arg];
|
|
3173
|
+
// Validate all names first
|
|
3174
|
+
const valid = [];
|
|
3175
|
+
for (const name of names) {
|
|
3176
|
+
const teammateDir = join(this.teammatesDir, name);
|
|
3177
|
+
try {
|
|
3178
|
+
const s = await stat(teammateDir);
|
|
3179
|
+
if (!s.isDirectory()) {
|
|
3180
|
+
this.feedLine(tp.warning(` ${name}: not a directory, skipping`));
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
valid.push(name);
|
|
3184
|
+
}
|
|
3185
|
+
catch {
|
|
3186
|
+
this.feedLine(tp.warning(` ${name}: no directory found, skipping`));
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
if (valid.length === 0)
|
|
3190
|
+
return;
|
|
3191
|
+
// Queue a compact task for each teammate
|
|
3192
|
+
for (const name of valid) {
|
|
3193
|
+
this.taskQueue.push({
|
|
3194
|
+
type: "compact",
|
|
3195
|
+
teammate: name,
|
|
3196
|
+
task: "compact + index update",
|
|
3197
|
+
});
|
|
3198
|
+
}
|
|
3199
|
+
this.feedLine();
|
|
3200
|
+
this.feedLine(concat(tp.muted(" Queued compaction for "), tp.accent(valid.map((n) => `@${n}`).join(", ")), tp.muted(` (${valid.length} task${valid.length === 1 ? "" : "s"})`)));
|
|
3201
|
+
this.feedLine();
|
|
3202
|
+
this.refreshView();
|
|
3203
|
+
// Start draining
|
|
3204
|
+
this.kickDrain();
|
|
3205
|
+
}
|
|
3206
|
+
/** Run compaction + recall index update for a single teammate. */
|
|
3207
|
+
async runCompact(name) {
|
|
3208
|
+
const teammateDir = join(this.teammatesDir, name);
|
|
3209
|
+
if (this.chatView) {
|
|
3210
|
+
this.chatView.setProgress(`Compacting ${name}...`);
|
|
3211
|
+
this.refreshView();
|
|
3212
|
+
}
|
|
3213
|
+
let spinner = null;
|
|
3214
|
+
if (!this.chatView) {
|
|
3215
|
+
spinner = ora({ text: `Compacting ${name}...`, color: "cyan" }).start();
|
|
3216
|
+
}
|
|
3217
|
+
try {
|
|
3218
|
+
const result = await compactEpisodic(teammateDir, name);
|
|
3219
|
+
const parts = [];
|
|
3220
|
+
if (result.weekliesCreated.length > 0) {
|
|
3221
|
+
parts.push(`${result.weekliesCreated.length} weekly summaries created`);
|
|
3222
|
+
}
|
|
3223
|
+
if (result.monthliesCreated.length > 0) {
|
|
3224
|
+
parts.push(`${result.monthliesCreated.length} monthly summaries created`);
|
|
3225
|
+
}
|
|
3226
|
+
if (result.dailiesRemoved.length > 0) {
|
|
3227
|
+
parts.push(`${result.dailiesRemoved.length} daily logs compacted`);
|
|
3228
|
+
}
|
|
3229
|
+
if (result.weekliesRemoved.length > 0) {
|
|
3230
|
+
parts.push(`${result.weekliesRemoved.length} old weekly summaries archived`);
|
|
3231
|
+
}
|
|
3232
|
+
if (parts.length === 0) {
|
|
3233
|
+
if (spinner)
|
|
3234
|
+
spinner.info(`${name}: nothing to compact`);
|
|
3235
|
+
if (this.chatView)
|
|
3236
|
+
this.feedLine(tp.muted(` ℹ ${name}: nothing to compact`));
|
|
3237
|
+
}
|
|
3238
|
+
else {
|
|
3239
|
+
if (spinner)
|
|
3240
|
+
spinner.succeed(`${name}: ${parts.join(", ")}`);
|
|
3241
|
+
if (this.chatView)
|
|
3242
|
+
this.feedLine(tp.success(` ✔ ${name}: ${parts.join(", ")}`));
|
|
3243
|
+
}
|
|
3244
|
+
if (this.chatView)
|
|
3245
|
+
this.chatView.setProgress(null);
|
|
3246
|
+
// Trigger recall sync if installed
|
|
3247
|
+
try {
|
|
3248
|
+
const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
|
|
3249
|
+
if (svcJson && "recall" in svcJson) {
|
|
3250
|
+
if (this.chatView) {
|
|
3251
|
+
this.chatView.setProgress(`Syncing ${name} index...`);
|
|
3252
|
+
this.refreshView();
|
|
3253
|
+
}
|
|
3254
|
+
let syncSpinner = null;
|
|
3255
|
+
if (!this.chatView) {
|
|
3256
|
+
syncSpinner = ora({
|
|
3257
|
+
text: `Syncing ${name} index...`,
|
|
3258
|
+
color: "cyan",
|
|
3259
|
+
}).start();
|
|
3260
|
+
}
|
|
3261
|
+
await execAsync(`teammates-recall sync --dir "${this.teammatesDir}"`);
|
|
3262
|
+
if (syncSpinner)
|
|
3263
|
+
syncSpinner.succeed(`${name}: index synced`);
|
|
3264
|
+
if (this.chatView) {
|
|
3265
|
+
this.chatView.setProgress(null);
|
|
3266
|
+
this.feedLine(tp.success(` ✔ ${name}: index synced`));
|
|
3267
|
+
}
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
catch {
|
|
3271
|
+
/* recall not installed or sync failed — non-fatal */
|
|
3272
|
+
}
|
|
3273
|
+
}
|
|
3274
|
+
catch (err) {
|
|
3275
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3276
|
+
if (spinner)
|
|
3277
|
+
spinner.fail(`${name}: ${msg}`);
|
|
3278
|
+
if (this.chatView) {
|
|
3279
|
+
this.chatView.setProgress(null);
|
|
3280
|
+
this.feedLine(tp.error(` ✖ ${name}: ${msg}`));
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
this.refreshView();
|
|
3284
|
+
}
|
|
3285
|
+
async cmdRetro(argsStr) {
|
|
3286
|
+
const arg = argsStr.trim().replace(/^@/, "");
|
|
3287
|
+
// Resolve target list
|
|
3288
|
+
const allTeammates = this.orchestrator
|
|
3289
|
+
.listTeammates()
|
|
3290
|
+
.filter((n) => n !== this.adapterName);
|
|
3291
|
+
let targets;
|
|
3292
|
+
if (arg === "everyone") {
|
|
3293
|
+
targets = allTeammates;
|
|
3294
|
+
}
|
|
3295
|
+
else if (arg) {
|
|
3296
|
+
// Validate teammate exists
|
|
3297
|
+
const names = this.orchestrator.listTeammates();
|
|
3298
|
+
if (!names.includes(arg)) {
|
|
3299
|
+
this.feedLine(tp.warning(` Unknown teammate: @${arg}`));
|
|
3300
|
+
this.refreshView();
|
|
3301
|
+
return;
|
|
3302
|
+
}
|
|
3303
|
+
targets = [arg];
|
|
3304
|
+
}
|
|
3305
|
+
else if (this.lastResult) {
|
|
3306
|
+
targets = [this.lastResult.teammate];
|
|
3307
|
+
}
|
|
3308
|
+
else {
|
|
3309
|
+
this.feedLine(tp.warning(" No teammate specified and no recent task to infer from."));
|
|
3310
|
+
this.feedLine(tp.muted(" Usage: /retro <teammate>"));
|
|
3311
|
+
this.refreshView();
|
|
3312
|
+
return;
|
|
3313
|
+
}
|
|
3314
|
+
const retroPrompt = `Run a structured self-retrospective. Review your SOUL.md, WISDOM.md, your last 2-3 weekly summaries (or last 7 daily logs if no weeklies exist), and any typed memories in your memory/ folder.
|
|
3315
|
+
|
|
3316
|
+
Produce a response with these four sections:
|
|
3317
|
+
|
|
3318
|
+
## 1. What's Working
|
|
3319
|
+
Things you do well, based on evidence from recent work. Patterns worth reinforcing or codifying into wisdom. Cite specific examples from daily logs or memories.
|
|
3320
|
+
|
|
3321
|
+
## 2. What's Not Working
|
|
3322
|
+
Friction, recurring issues, or patterns that aren't serving the project. Be specific — cite examples from daily logs or memories if possible.
|
|
3323
|
+
|
|
3324
|
+
## 3. Proposed SOUL.md Changes
|
|
3325
|
+
The core output. Each proposal is a **specific edit** to your SOUL.md. Use this exact format for each proposal:
|
|
3326
|
+
|
|
3327
|
+
**Proposal N: <short title>**
|
|
3328
|
+
- **Section:** <which SOUL.md section to change, e.g. Boundaries, Core Principles, Ownership>
|
|
3329
|
+
- **Before:** <the current text to replace, or "(new entry)" if adding>
|
|
3330
|
+
- **After:** <the exact replacement text>
|
|
3331
|
+
- **Why:** <evidence from recent work justifying the change>
|
|
3332
|
+
|
|
3333
|
+
Only propose changes to your own SOUL.md. If a change affects shared files, note that it needs a handoff.
|
|
3334
|
+
|
|
3335
|
+
## 4. Questions for the Team
|
|
3336
|
+
Issues that can't be resolved unilaterally — they need input from other teammates or the user.
|
|
3337
|
+
|
|
3338
|
+
**Rules:**
|
|
3339
|
+
- This is a self-review of YOUR work. Do not evaluate other teammates.
|
|
3340
|
+
- Evidence over opinion — cite specific examples.
|
|
3341
|
+
- No busywork — if everything is working well, say "all good, no changes." That's a valid outcome.
|
|
3342
|
+
- Number each proposal (Proposal 1, Proposal 2, etc.) so the user can approve or reject individually.`;
|
|
3343
|
+
const label = targets.length > 1
|
|
3344
|
+
? targets.map((n) => `@${n}`).join(", ")
|
|
3345
|
+
: `@${targets[0]}`;
|
|
3346
|
+
this.feedLine();
|
|
3347
|
+
this.feedLine(concat(tp.muted(" Queued retro for "), tp.accent(label)));
|
|
3348
|
+
this.feedLine();
|
|
3349
|
+
this.refreshView();
|
|
3350
|
+
for (const name of targets) {
|
|
3351
|
+
this.taskQueue.push({ type: "retro", teammate: name, task: retroPrompt });
|
|
3352
|
+
}
|
|
3353
|
+
this.kickDrain();
|
|
3354
|
+
}
|
|
3355
|
+
/**
|
|
3356
|
+
* Background startup maintenance:
|
|
3357
|
+
* 1. Scan all teammates for daily logs older than a week → compact them
|
|
3358
|
+
* 2. Sync recall indexes if recall is installed
|
|
3359
|
+
*/
|
|
3360
|
+
/** Recursively delete files/directories older than maxAgeMs. Removes empty parent dirs. */
|
|
3361
|
+
async cleanOldTempFiles(dir, maxAgeMs) {
|
|
3362
|
+
const now = Date.now();
|
|
3363
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
3364
|
+
for (const entry of entries) {
|
|
3365
|
+
const fullPath = join(dir, entry.name);
|
|
3366
|
+
if (entry.isDirectory()) {
|
|
3367
|
+
await this.cleanOldTempFiles(fullPath, maxAgeMs);
|
|
3368
|
+
// Remove dir if now empty
|
|
3369
|
+
const remaining = await readdir(fullPath).catch(() => [""]);
|
|
3370
|
+
if (remaining.length === 0)
|
|
3371
|
+
await rm(fullPath, { recursive: true }).catch(() => { });
|
|
3372
|
+
}
|
|
3373
|
+
else {
|
|
3374
|
+
const info = await stat(fullPath).catch(() => null);
|
|
3375
|
+
if (info && now - info.mtimeMs > maxAgeMs) {
|
|
3376
|
+
await unlink(fullPath).catch(() => { });
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
async startupMaintenance() {
|
|
3382
|
+
// Clean up .teammates/.tmp files older than 1 week
|
|
3383
|
+
const tmpDir = join(this.teammatesDir, ".tmp");
|
|
3384
|
+
try {
|
|
3385
|
+
await this.cleanOldTempFiles(tmpDir, 7 * 24 * 60 * 60 * 1000);
|
|
3386
|
+
}
|
|
3387
|
+
catch {
|
|
3388
|
+
/* .tmp dir may not exist yet — non-fatal */
|
|
3389
|
+
}
|
|
3390
|
+
const teammates = this.orchestrator
|
|
3391
|
+
.listTeammates()
|
|
3392
|
+
.filter((n) => n !== this.adapterName);
|
|
3393
|
+
if (teammates.length === 0)
|
|
3394
|
+
return;
|
|
3395
|
+
// Check if recall is installed
|
|
3396
|
+
let recallInstalled = false;
|
|
3397
|
+
try {
|
|
3398
|
+
const svcJson = JSON.parse(readFileSync(join(this.teammatesDir, "services.json"), "utf-8"));
|
|
3399
|
+
recallInstalled = !!(svcJson && "recall" in svcJson);
|
|
3400
|
+
}
|
|
3401
|
+
catch {
|
|
3402
|
+
/* no services.json */
|
|
3403
|
+
}
|
|
3404
|
+
// 1. Check each teammate for stale daily logs (older than 7 days)
|
|
3405
|
+
const oneWeekAgo = new Date();
|
|
3406
|
+
oneWeekAgo.setDate(oneWeekAgo.getDate() - 7);
|
|
3407
|
+
const cutoff = oneWeekAgo.toISOString().slice(0, 10); // YYYY-MM-DD
|
|
3408
|
+
const needsCompact = [];
|
|
3409
|
+
for (const name of teammates) {
|
|
3410
|
+
const memoryDir = join(this.teammatesDir, name, "memory");
|
|
3411
|
+
try {
|
|
3412
|
+
const entries = await readdir(memoryDir);
|
|
3413
|
+
const hasStale = entries.some((e) => {
|
|
3414
|
+
if (!e.endsWith(".md"))
|
|
3415
|
+
return false;
|
|
3416
|
+
const stem = e.replace(".md", "");
|
|
3417
|
+
return /^\d{4}-\d{2}-\d{2}$/.test(stem) && stem < cutoff;
|
|
3418
|
+
});
|
|
3419
|
+
if (hasStale)
|
|
3420
|
+
needsCompact.push(name);
|
|
3421
|
+
}
|
|
3422
|
+
catch {
|
|
3423
|
+
/* no memory dir */
|
|
3424
|
+
}
|
|
3425
|
+
}
|
|
3426
|
+
if (needsCompact.length > 0) {
|
|
3427
|
+
this.feedLine(concat(tp.muted(" Compacting stale logs for "), tp.accent(needsCompact.map((n) => `@${n}`).join(", ")), tp.muted("...")));
|
|
3428
|
+
this.refreshView();
|
|
3429
|
+
for (const name of needsCompact) {
|
|
3430
|
+
await this.runCompact(name);
|
|
3431
|
+
}
|
|
3432
|
+
}
|
|
3433
|
+
// 2. Sync recall indexes if installed
|
|
3434
|
+
if (recallInstalled) {
|
|
3435
|
+
try {
|
|
3436
|
+
await execAsync(`teammates-recall sync --dir "${this.teammatesDir}"`);
|
|
3437
|
+
}
|
|
3438
|
+
catch {
|
|
3439
|
+
/* sync failed — non-fatal */
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
}
|
|
3443
|
+
async cmdCopy() {
|
|
3444
|
+
this.doCopy(); // copies entire session
|
|
3445
|
+
}
|
|
3446
|
+
/** Build the full chat session as a markdown document. */
|
|
3447
|
+
buildSessionMarkdown() {
|
|
3448
|
+
if (this.conversationHistory.length === 0)
|
|
3449
|
+
return "";
|
|
3450
|
+
const lines = [];
|
|
3451
|
+
lines.push(`# Chat Session\n`);
|
|
3452
|
+
for (const entry of this.conversationHistory) {
|
|
3453
|
+
if (entry.role === "user") {
|
|
3454
|
+
lines.push(`**User:** ${entry.text}\n`);
|
|
3455
|
+
}
|
|
3456
|
+
else {
|
|
3457
|
+
// Strip protocol artifacts from the raw output
|
|
3458
|
+
const cleaned = entry.text
|
|
3459
|
+
.replace(/^TO:\s*\S+\s*\n/im, "")
|
|
3460
|
+
.replace(/```json\s*\n\s*\{[\s\S]*?\}\s*\n\s*```\s*$/g, "")
|
|
3461
|
+
.trim();
|
|
3462
|
+
lines.push(`**${entry.role}:**\n\n${cleaned}\n`);
|
|
3463
|
+
}
|
|
3464
|
+
lines.push("---\n");
|
|
3465
|
+
}
|
|
3466
|
+
return lines.join("\n");
|
|
3467
|
+
}
|
|
3468
|
+
doCopy(content) {
|
|
3469
|
+
// Build content: if none specified, export the entire chat session as markdown
|
|
3470
|
+
const text = content ?? this.buildSessionMarkdown();
|
|
3471
|
+
if (!text) {
|
|
3472
|
+
this.feedLine(tp.muted(" Nothing to copy."));
|
|
3473
|
+
this.refreshView();
|
|
3474
|
+
return;
|
|
3475
|
+
}
|
|
3476
|
+
try {
|
|
3477
|
+
const isWin = process.platform === "win32";
|
|
3478
|
+
const cmd = isWin
|
|
3479
|
+
? "clip"
|
|
3480
|
+
: process.platform === "darwin"
|
|
3481
|
+
? "pbcopy"
|
|
3482
|
+
: "xclip -selection clipboard";
|
|
3483
|
+
const child = execCb(cmd, () => { });
|
|
3484
|
+
child.stdin?.write(text);
|
|
3485
|
+
child.stdin?.end();
|
|
3486
|
+
// Show brief "Copied" message in the progress area
|
|
3487
|
+
if (this.chatView) {
|
|
3488
|
+
this.chatView.setProgress(concat(tp.success("✔ "), tp.muted("Copied to clipboard")));
|
|
3489
|
+
this.refreshView();
|
|
3490
|
+
setTimeout(() => {
|
|
3491
|
+
this.chatView.setProgress(null);
|
|
3492
|
+
this.refreshView();
|
|
3493
|
+
}, 1500);
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
catch {
|
|
3497
|
+
if (this.chatView) {
|
|
3498
|
+
this.chatView.setProgress(concat(tp.error("✖ "), tp.muted("Failed to copy")));
|
|
3499
|
+
this.refreshView();
|
|
3500
|
+
setTimeout(() => {
|
|
3501
|
+
this.chatView.setProgress(null);
|
|
3502
|
+
this.refreshView();
|
|
3503
|
+
}, 1500);
|
|
3504
|
+
}
|
|
3505
|
+
}
|
|
1465
3506
|
}
|
|
1466
3507
|
async cmdHelp() {
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
3508
|
+
this.feedLine();
|
|
3509
|
+
this.feedLine(tp.bold(" Commands"));
|
|
3510
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
1470
3511
|
// De-duplicate (aliases map to same command)
|
|
1471
3512
|
const seen = new Set();
|
|
1472
3513
|
for (const [, cmd] of this.commands) {
|
|
@@ -1474,44 +3515,202 @@ class TeammatesREPL {
|
|
|
1474
3515
|
continue;
|
|
1475
3516
|
seen.add(cmd.name);
|
|
1476
3517
|
const aliases = cmd.aliases.length > 0
|
|
1477
|
-
?
|
|
3518
|
+
? ` (${cmd.aliases.map((a) => `/${a}`).join(", ")})`
|
|
1478
3519
|
: "";
|
|
1479
|
-
|
|
3520
|
+
this.feedLine(concat(tp.accent(` ${cmd.usage}`.padEnd(36)), pen(cmd.description), tp.muted(aliases)));
|
|
1480
3521
|
}
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
3522
|
+
this.feedLine();
|
|
3523
|
+
this.feedLine(concat(tp.muted(" Tip: "), tp.text("Type text without / to auto-route to the best teammate")));
|
|
3524
|
+
this.feedLine(concat(tp.muted(" Tip: "), tp.text("Press Tab to autocomplete commands and teammate names")));
|
|
3525
|
+
this.feedLine();
|
|
3526
|
+
this.refreshView();
|
|
3527
|
+
}
|
|
3528
|
+
async cmdUser(argsStr) {
|
|
3529
|
+
const userMdPath = join(this.teammatesDir, "USER.md");
|
|
3530
|
+
const change = argsStr.trim();
|
|
3531
|
+
if (!change) {
|
|
3532
|
+
// No args — print current USER.md
|
|
3533
|
+
let content;
|
|
3534
|
+
try {
|
|
3535
|
+
content = readFileSync(userMdPath, "utf-8");
|
|
3536
|
+
}
|
|
3537
|
+
catch {
|
|
3538
|
+
this.feedLine(tp.muted(" USER.md not found."));
|
|
3539
|
+
this.feedLine(tp.muted(" Run /init or create .teammates/USER.md manually."));
|
|
3540
|
+
this.refreshView();
|
|
3541
|
+
return;
|
|
3542
|
+
}
|
|
3543
|
+
if (!content.trim()) {
|
|
3544
|
+
this.feedLine(tp.muted(" USER.md is empty."));
|
|
3545
|
+
this.refreshView();
|
|
3546
|
+
return;
|
|
3547
|
+
}
|
|
3548
|
+
this.feedLine();
|
|
3549
|
+
this.feedLine(tp.muted(" ── USER.md ──"));
|
|
3550
|
+
this.feedLine();
|
|
3551
|
+
this.feedMarkdown(content);
|
|
3552
|
+
this.feedLine();
|
|
3553
|
+
this.feedLine(tp.muted(" ── end ──"));
|
|
3554
|
+
this.feedLine();
|
|
3555
|
+
this.refreshView();
|
|
3556
|
+
return;
|
|
3557
|
+
}
|
|
3558
|
+
// Has args — queue a task to the coding agent to apply the change
|
|
3559
|
+
const task = `Update the file ${userMdPath} with the following change:\n\n${change}\n\nKeep the existing content intact unless the change explicitly replaces something. This is the user's profile — be concise and accurate.`;
|
|
3560
|
+
this.taskQueue.push({ type: "agent", teammate: this.adapterName, task });
|
|
3561
|
+
this.feedLine(concat(tp.muted(" Queued USER.md update → "), tp.accent(`@${this.adapterName}`)));
|
|
3562
|
+
this.feedLine();
|
|
3563
|
+
this.refreshView();
|
|
3564
|
+
this.kickDrain();
|
|
3565
|
+
}
|
|
3566
|
+
async cmdBtw(argsStr) {
|
|
3567
|
+
const question = argsStr.trim();
|
|
3568
|
+
if (!question) {
|
|
3569
|
+
this.feedLine(tp.muted(" Usage: /btw <question>"));
|
|
3570
|
+
this.refreshView();
|
|
3571
|
+
return;
|
|
3572
|
+
}
|
|
3573
|
+
this.taskQueue.push({
|
|
3574
|
+
type: "btw",
|
|
3575
|
+
teammate: this.adapterName,
|
|
3576
|
+
task: question,
|
|
3577
|
+
});
|
|
3578
|
+
this.feedLine(concat(tp.muted(" Side question → "), tp.accent(`@${this.adapterName}`)));
|
|
3579
|
+
this.feedLine();
|
|
3580
|
+
this.refreshView();
|
|
3581
|
+
this.kickDrain();
|
|
3582
|
+
}
|
|
3583
|
+
async cmdTheme() {
|
|
3584
|
+
const t = theme();
|
|
3585
|
+
this.feedLine();
|
|
3586
|
+
this.feedLine(tp.bold(" Theme"));
|
|
3587
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
3588
|
+
this.feedLine();
|
|
3589
|
+
// Helper: show a swatch + variable name + hex + example text
|
|
3590
|
+
const row = (name, c, example) => {
|
|
3591
|
+
const hex = colorToHex(c);
|
|
3592
|
+
this.feedLine(concat(pen.fg(c)(" ██"), tp.text(` ${name}`.padEnd(24)), tp.muted(hex.padEnd(12)), pen.fg(c)(example)));
|
|
3593
|
+
};
|
|
3594
|
+
this.feedLine(tp.muted(" Variable Hex Example"));
|
|
3595
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
3596
|
+
// Brand / accent
|
|
3597
|
+
row("accent", t.accent, "@beacon /status ● teammate");
|
|
3598
|
+
row("accentBright", t.accentBright, "▸ highlighted item");
|
|
3599
|
+
row("accentDim", t.accentDim, "┌─── border ───┐");
|
|
3600
|
+
this.feedLine();
|
|
3601
|
+
// Foreground
|
|
3602
|
+
row("text", t.text, "Primary text content");
|
|
3603
|
+
row("textMuted", t.textMuted, "Description or secondary info");
|
|
3604
|
+
row("textDim", t.textDim, "─── separator ───");
|
|
3605
|
+
this.feedLine();
|
|
3606
|
+
// Status
|
|
3607
|
+
row("success", t.success, "✔ Task completed");
|
|
3608
|
+
row("warning", t.warning, "⚠ Pending handoff");
|
|
3609
|
+
row("error", t.error, "✖ Something went wrong");
|
|
3610
|
+
row("info", t.info, "⠋ Working on task...");
|
|
3611
|
+
this.feedLine();
|
|
3612
|
+
// Interactive
|
|
3613
|
+
row("prompt", t.prompt, "> ");
|
|
3614
|
+
row("input", t.input, "user typed text");
|
|
3615
|
+
row("separator", t.separator, "────────────────");
|
|
3616
|
+
row("progress", t.progress, "analyzing codebase...");
|
|
3617
|
+
row("dropdown", t.dropdown, "/status session overview");
|
|
3618
|
+
row("dropdownHighlight", t.dropdownHighlight, "▸ /help all commands");
|
|
3619
|
+
this.feedLine();
|
|
3620
|
+
// Cursor
|
|
3621
|
+
this.feedLine(concat(pen.fg(t.cursorFg).bg(t.cursorBg)(" ██"), tp.text(" cursorFg/cursorBg".padEnd(24)), tp.muted(`${colorToHex(t.cursorFg)}/${colorToHex(t.cursorBg)}`.padEnd(12)), pen.fg(t.cursorFg).bg(t.cursorBg)(" block cursor ")));
|
|
3622
|
+
this.feedLine();
|
|
3623
|
+
this.feedLine(tp.muted(" Base accent: #3A96DD"));
|
|
3624
|
+
this.feedLine();
|
|
3625
|
+
// ── Markdown preview ──────────────────────────────────────
|
|
3626
|
+
this.feedLine(tp.bold(" Markdown Preview"));
|
|
3627
|
+
this.feedLine(tp.muted(` ${"─".repeat(50)}`));
|
|
3628
|
+
this.feedLine();
|
|
3629
|
+
const mdSample = [
|
|
3630
|
+
"# Heading 1",
|
|
3631
|
+
"",
|
|
3632
|
+
"## Heading 2",
|
|
3633
|
+
"",
|
|
3634
|
+
"### Heading 3",
|
|
3635
|
+
"",
|
|
3636
|
+
"Regular text with **bold**, *italic*, and `inline code`.",
|
|
3637
|
+
"A [link](https://example.com) and ~~strikethrough~~.",
|
|
3638
|
+
"",
|
|
3639
|
+
"- Bullet item one",
|
|
3640
|
+
"- Bullet item with **bold**",
|
|
3641
|
+
" - Nested item",
|
|
3642
|
+
"",
|
|
3643
|
+
"1. Ordered first",
|
|
3644
|
+
"2. Ordered second",
|
|
3645
|
+
"",
|
|
3646
|
+
"> Blockquote text",
|
|
3647
|
+
"> across multiple lines",
|
|
3648
|
+
"",
|
|
3649
|
+
"```js",
|
|
3650
|
+
'const greeting = "hello";',
|
|
3651
|
+
"async function main() {",
|
|
3652
|
+
' await fetch("/api");',
|
|
3653
|
+
" return 42;",
|
|
3654
|
+
"}",
|
|
3655
|
+
"```",
|
|
3656
|
+
"",
|
|
3657
|
+
"```python",
|
|
3658
|
+
"def greet(name: str) -> None:",
|
|
3659
|
+
' print(f"Hello, {name}")',
|
|
3660
|
+
"```",
|
|
3661
|
+
"",
|
|
3662
|
+
"```bash",
|
|
3663
|
+
'echo "$HOME" | grep --color user',
|
|
3664
|
+
"if [ -f .env ]; then source .env; fi",
|
|
3665
|
+
"```",
|
|
3666
|
+
"",
|
|
3667
|
+
"```json",
|
|
3668
|
+
"{",
|
|
3669
|
+
' "name": "teammates",',
|
|
3670
|
+
' "version": "0.1.0",',
|
|
3671
|
+
' "active": true',
|
|
3672
|
+
"}",
|
|
3673
|
+
"```",
|
|
3674
|
+
"",
|
|
3675
|
+
"| Language | Status |",
|
|
3676
|
+
"|------------|---------|",
|
|
3677
|
+
"| JavaScript | ✔ Ready |",
|
|
3678
|
+
"| Python | ✔ Ready |",
|
|
3679
|
+
"| C# | ✔ Ready |",
|
|
3680
|
+
"",
|
|
3681
|
+
"---",
|
|
3682
|
+
].join("\n");
|
|
3683
|
+
this.feedMarkdown(mdSample);
|
|
3684
|
+
this.feedLine();
|
|
3685
|
+
this.refreshView();
|
|
1487
3686
|
}
|
|
1488
3687
|
}
|
|
1489
3688
|
// ─── Usage (non-interactive) ─────────────────────────────────────────
|
|
1490
3689
|
function printUsage() {
|
|
1491
|
-
console.log(`
|
|
1492
|
-
${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
|
|
1493
|
-
|
|
1494
|
-
${chalk.bold("Usage:")}
|
|
1495
|
-
teammates <agent> Launch session with an agent
|
|
1496
|
-
teammates claude Use Claude Code
|
|
1497
|
-
teammates codex Use OpenAI Codex
|
|
1498
|
-
teammates aider Use Aider
|
|
1499
|
-
|
|
1500
|
-
${chalk.bold("Options:")}
|
|
1501
|
-
--model <model> Override the agent model
|
|
1502
|
-
--dir <path> Override .teammates/ location
|
|
1503
|
-
|
|
1504
|
-
${chalk.bold("Agents:")}
|
|
1505
|
-
claude Claude Code CLI (requires 'claude' on PATH)
|
|
1506
|
-
codex OpenAI Codex CLI (requires 'codex' on PATH)
|
|
1507
|
-
aider Aider CLI (requires 'aider' on PATH)
|
|
1508
|
-
echo Test adapter — echoes prompts (no external agent)
|
|
1509
|
-
|
|
1510
|
-
${chalk.bold("In-session:")}
|
|
1511
|
-
@teammate <task> Assign directly via @mention
|
|
1512
|
-
<text> Auto-route to the best teammate
|
|
1513
|
-
/status Session overview
|
|
1514
|
-
/help All commands
|
|
3690
|
+
console.log(`
|
|
3691
|
+
${chalk.bold("@teammates/cli")} — Agent-agnostic teammate orchestrator
|
|
3692
|
+
|
|
3693
|
+
${chalk.bold("Usage:")}
|
|
3694
|
+
teammates <agent> Launch session with an agent
|
|
3695
|
+
teammates claude Use Claude Code
|
|
3696
|
+
teammates codex Use OpenAI Codex
|
|
3697
|
+
teammates aider Use Aider
|
|
3698
|
+
|
|
3699
|
+
${chalk.bold("Options:")}
|
|
3700
|
+
--model <model> Override the agent model
|
|
3701
|
+
--dir <path> Override .teammates/ location
|
|
3702
|
+
|
|
3703
|
+
${chalk.bold("Agents:")}
|
|
3704
|
+
claude Claude Code CLI (requires 'claude' on PATH)
|
|
3705
|
+
codex OpenAI Codex CLI (requires 'codex' on PATH)
|
|
3706
|
+
aider Aider CLI (requires 'aider' on PATH)
|
|
3707
|
+
echo Test adapter — echoes prompts (no external agent)
|
|
3708
|
+
|
|
3709
|
+
${chalk.bold("In-session:")}
|
|
3710
|
+
@teammate <task> Assign directly via @mention
|
|
3711
|
+
<text> Auto-route to the best teammate
|
|
3712
|
+
/status Session overview
|
|
3713
|
+
/help All commands
|
|
1515
3714
|
`.trim());
|
|
1516
3715
|
}
|
|
1517
3716
|
// ─── Main ────────────────────────────────────────────────────────────
|