@dungle-scrubs/tallow 0.8.24 → 0.8.26
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/dist/auth-hardening.d.ts +12 -0
- package/dist/auth-hardening.d.ts.map +1 -1
- package/dist/auth-hardening.js +30 -7
- package/dist/auth-hardening.js.map +1 -1
- package/dist/cli.js +5 -0
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +1 -1
- package/dist/install.js +2 -2
- package/dist/install.js.map +1 -1
- package/dist/interactive-mode-patch.d.ts.map +1 -1
- package/dist/interactive-mode-patch.js +119 -7
- package/dist/interactive-mode-patch.js.map +1 -1
- package/dist/model-metadata-overrides.d.ts +19 -0
- package/dist/model-metadata-overrides.d.ts.map +1 -0
- package/dist/model-metadata-overrides.js +38 -0
- package/dist/model-metadata-overrides.js.map +1 -0
- package/dist/sdk.d.ts +2 -0
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +28 -1
- package/dist/sdk.js.map +1 -1
- package/extensions/__integration__/teams-runtime.test.ts +22 -1
- package/extensions/_shared/__tests__/shell-policy.test.ts +197 -0
- package/extensions/_shared/shell-policy.ts +27 -0
- package/extensions/background-task-tool/index.ts +2 -1
- package/extensions/bash-tool-enhanced/index.ts +2 -1
- package/extensions/custom-footer/__tests__/index.test.ts +29 -0
- package/extensions/custom-footer/context-display.ts +49 -0
- package/extensions/custom-footer/index.ts +10 -23
- package/extensions/permissions/index.ts +31 -10
- package/extensions/plan-mode-tool/__tests__/index.test.ts +32 -2
- package/extensions/plan-mode-tool/index.ts +6 -1
- package/extensions/skill-commands/__tests__/shared-skills-dirs.test.ts +113 -0
- package/extensions/skill-commands/index.ts +62 -5
- package/extensions/slash-command-bridge/index.ts +30 -1
- package/extensions/subagent-tool/__tests__/process-liveness.test.ts +42 -3
- package/extensions/subagent-tool/process.ts +132 -21
- package/extensions/tasks/__tests__/store.test.ts +26 -2
- package/extensions/tasks/commands/register-tasks-extension.ts +2 -2
- package/extensions/tasks/index.ts +5 -5
- package/extensions/tasks/state/index.ts +90 -36
- package/extensions/teams-tool/__tests__/archive-store.test.ts +98 -0
- package/extensions/teams-tool/__tests__/peer-messaging.test.ts +26 -0
- package/extensions/teams-tool/archive-store.ts +200 -0
- package/extensions/teams-tool/sessions/spawn.ts +244 -71
- package/extensions/teams-tool/tools/register-extension.ts +146 -105
- package/extensions/teams-tool/tools/teammate-tools.ts +43 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
- package/node_modules/@mariozechner/pi-tui/dist/keys.js +59 -7
- package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
- package/node_modules/@mariozechner/pi-tui/package.json +1 -1
- package/node_modules/@mariozechner/pi-tui/src/keys.ts +71 -7
- package/package.json +5 -5
- package/skills/tallow-expert/SKILL.md +1 -1
- package/templates/agents/architect.md +13 -5
- package/templates/agents/debug.md +3 -3
- package/templates/agents/explore.md +9 -2
- package/templates/agents/refactor.md +2 -2
- package/templates/agents/scout.md +3 -2
- package/extensions/__integration__/plan-rejection-feedback.test.ts +0 -272
|
@@ -23,6 +23,11 @@ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-age
|
|
|
23
23
|
import { Key, Loader, Text, type TUI } from "@mariozechner/pi-tui";
|
|
24
24
|
import { Type } from "@sinclair/typebox";
|
|
25
25
|
import { INTEROP_EVENT_NAMES, onInteropEvent } from "../../_shared/interop-events.js";
|
|
26
|
+
import {
|
|
27
|
+
deleteArchivedTeamFromDisk,
|
|
28
|
+
loadAllArchivedTeamsFromDisk,
|
|
29
|
+
writeArchivedTeamToDisk,
|
|
30
|
+
} from "../archive-store.js";
|
|
26
31
|
import {
|
|
27
32
|
appendDashboardFeedEvent,
|
|
28
33
|
bindDashboardSessionTracking,
|
|
@@ -66,7 +71,6 @@ import {
|
|
|
66
71
|
export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
67
72
|
setInteropEvents(pi.events);
|
|
68
73
|
let cwd = process.cwd();
|
|
69
|
-
let dashboardCancelInFlight = false;
|
|
70
74
|
let dashboardEnabled = false;
|
|
71
75
|
let dashboardTicker: ReturnType<typeof setInterval> | undefined;
|
|
72
76
|
let dashboardTui: TUI | undefined;
|
|
@@ -154,67 +158,82 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
154
158
|
}
|
|
155
159
|
|
|
156
160
|
/**
|
|
157
|
-
*
|
|
158
|
-
*
|
|
161
|
+
* Refresh the in-memory archived-team index from disk.
|
|
162
|
+
*
|
|
163
|
+
* Team archives must survive process restarts, so the on-disk view is the
|
|
164
|
+
* source of truth. This helper keeps the legacy in-memory map synchronized
|
|
165
|
+
* for the existing store/query helpers.
|
|
166
|
+
*
|
|
167
|
+
* @returns void
|
|
159
168
|
*/
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
running.push({ teammate, team });
|
|
166
|
-
}
|
|
169
|
+
function refreshArchivedTeamStore(): void {
|
|
170
|
+
const archives = getArchivedTeams();
|
|
171
|
+
archives.clear();
|
|
172
|
+
for (const archived of loadAllArchivedTeamsFromDisk()) {
|
|
173
|
+
archives.set(archived.name, archived);
|
|
167
174
|
}
|
|
168
|
-
|
|
175
|
+
}
|
|
169
176
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
);
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
dashboardActivity.touch(team.name, teammate.name);
|
|
185
|
-
appendDashboardFeedEvent(team.name, "orchestrator", teammate.name, "Cancelled run.");
|
|
186
|
-
touchedTeams.add(team);
|
|
177
|
+
/**
|
|
178
|
+
* Move an active team into the archive store and persist it to disk.
|
|
179
|
+
*
|
|
180
|
+
* @param teamName - Active team name to archive
|
|
181
|
+
* @returns void
|
|
182
|
+
*/
|
|
183
|
+
function archiveRuntimeTeam(teamName: string): void {
|
|
184
|
+
const archived = archiveTeam(teamName);
|
|
185
|
+
if (!archived) return;
|
|
186
|
+
try {
|
|
187
|
+
writeArchivedTeamToDisk(archived);
|
|
188
|
+
refreshArchivedTeamStore();
|
|
189
|
+
} catch (error) {
|
|
190
|
+
console.error(`Failed to persist archived team ${teamName}: ${error}`);
|
|
187
191
|
}
|
|
188
|
-
for (const team of touchedTeams) refreshTeamView(team);
|
|
189
|
-
notifyDashboardChanged();
|
|
190
|
-
return running.length;
|
|
191
192
|
}
|
|
192
193
|
|
|
193
194
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
* @
|
|
195
|
+
* Dispose teammate sessions for one team.
|
|
196
|
+
*
|
|
197
|
+
* @param team - Team whose live teammate sessions should be cleaned up
|
|
198
|
+
* @param abortStreaming - Whether currently streaming teammates should be aborted first
|
|
199
|
+
* @returns Number of teammates that were processed
|
|
197
200
|
*/
|
|
198
|
-
function
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
201
|
+
async function disposeTeamSessions(
|
|
202
|
+
team: Team<Teammate>,
|
|
203
|
+
abortStreaming: boolean
|
|
204
|
+
): Promise<number> {
|
|
205
|
+
let count = 0;
|
|
206
|
+
for (const [, mate] of team.teammates) {
|
|
202
207
|
try {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
ctx.ui.notify(
|
|
206
|
-
`Cancelled ${cancelled} running teammate${cancelled === 1 ? "" : "s"}. Press Esc again to close dashboard.`,
|
|
207
|
-
"warning"
|
|
208
|
-
);
|
|
209
|
-
return;
|
|
208
|
+
if (abortStreaming && mate.session.isStreaming) {
|
|
209
|
+
await mate.session.abort();
|
|
210
210
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
211
|
+
mate.unsubscribe?.();
|
|
212
|
+
mate.session.dispose();
|
|
213
|
+
} catch (error) {
|
|
214
|
+
console.error(`Failed to clean up teammate ${mate.name}: ${error}`);
|
|
214
215
|
} finally {
|
|
215
|
-
|
|
216
|
+
mate.status = "shutdown";
|
|
217
|
+
count++;
|
|
216
218
|
}
|
|
217
|
-
}
|
|
219
|
+
}
|
|
220
|
+
return count;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Handle Escape inside dashboard mode.
|
|
225
|
+
*
|
|
226
|
+
* The dashboard is a view, not an interrupt button. Escape should return to
|
|
227
|
+
* chat without killing teammates; explicit teardown goes through team_shutdown
|
|
228
|
+
* or normal session shutdown.
|
|
229
|
+
*
|
|
230
|
+
* @param ctx - Extension context
|
|
231
|
+
* @returns void
|
|
232
|
+
*/
|
|
233
|
+
function handleDashboardEscape(ctx: ExtensionContext): void {
|
|
234
|
+
if (!dashboardEnabled) return;
|
|
235
|
+
disableDashboard(ctx, false);
|
|
236
|
+
ctx.ui.notify("Team dashboard disabled. Teammates keep running.", "info");
|
|
218
237
|
}
|
|
219
238
|
|
|
220
239
|
/**
|
|
@@ -256,7 +275,6 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
256
275
|
* @returns void
|
|
257
276
|
*/
|
|
258
277
|
function disableDashboard(ctx: ExtensionContext, notify = true): void {
|
|
259
|
-
dashboardCancelInFlight = false;
|
|
260
278
|
dashboardEnabled = false;
|
|
261
279
|
stopDashboardTicker();
|
|
262
280
|
setDashboardRenderCallback(undefined);
|
|
@@ -289,6 +307,7 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
289
307
|
|
|
290
308
|
pi.on("session_start", async (_event, ctx) => {
|
|
291
309
|
cwd = ctx.cwd;
|
|
310
|
+
refreshArchivedTeamStore();
|
|
292
311
|
publishTeamSnapshots();
|
|
293
312
|
publishDashboardState();
|
|
294
313
|
});
|
|
@@ -301,20 +320,10 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
301
320
|
// Archive all teams on session shutdown (preserves tasks for future recovery)
|
|
302
321
|
pi.on("session_shutdown", async () => {
|
|
303
322
|
for (const [name, team] of getTeams() as Map<string, Team<Teammate>>) {
|
|
304
|
-
|
|
305
|
-
try {
|
|
306
|
-
if (mate.session.isStreaming) await mate.session.abort();
|
|
307
|
-
mate.unsubscribe?.();
|
|
308
|
-
mate.session.dispose();
|
|
309
|
-
} catch (err) {
|
|
310
|
-
console.error(`Failed to clean up teammate ${mate.name}: ${err}`);
|
|
311
|
-
}
|
|
312
|
-
mate.status = "shutdown";
|
|
313
|
-
}
|
|
323
|
+
await disposeTeamSessions(team, true);
|
|
314
324
|
removeTeamView(name);
|
|
315
|
-
|
|
325
|
+
archiveRuntimeTeam(name);
|
|
316
326
|
}
|
|
317
|
-
dashboardCancelInFlight = false;
|
|
318
327
|
dashboardEnabled = false;
|
|
319
328
|
stopDashboardTicker();
|
|
320
329
|
setDashboardRenderCallback(undefined);
|
|
@@ -334,21 +343,9 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
334
343
|
for (const [name, team] of getTeams() as Map<string, Team<Teammate>>) {
|
|
335
344
|
const hasActiveWork = [...team.teammates.values()].some((m) => m.status === "working");
|
|
336
345
|
if (hasActiveWork) continue;
|
|
337
|
-
|
|
338
|
-
// All teammates finished — clean up and archive
|
|
339
|
-
for (const [, mate] of team.teammates) {
|
|
340
|
-
if (mate.status === "idle") {
|
|
341
|
-
try {
|
|
342
|
-
mate.unsubscribe?.();
|
|
343
|
-
mate.session.dispose();
|
|
344
|
-
} catch {
|
|
345
|
-
// Best-effort cleanup
|
|
346
|
-
}
|
|
347
|
-
mate.status = "shutdown";
|
|
348
|
-
}
|
|
349
|
-
}
|
|
346
|
+
await disposeTeamSessions(team, false);
|
|
350
347
|
removeTeamView(name);
|
|
351
|
-
|
|
348
|
+
archiveRuntimeTeam(name);
|
|
352
349
|
}
|
|
353
350
|
});
|
|
354
351
|
|
|
@@ -388,6 +385,7 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
388
385
|
name: Type.String({ description: "Team name (unique)" }),
|
|
389
386
|
}),
|
|
390
387
|
async execute(_toolCallId, params) {
|
|
388
|
+
refreshArchivedTeamStore();
|
|
391
389
|
if (getTeams().has(params.name)) {
|
|
392
390
|
return {
|
|
393
391
|
content: [{ type: "text", text: `Team "${params.name}" already exists.` }],
|
|
@@ -395,6 +393,20 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
395
393
|
isError: true,
|
|
396
394
|
};
|
|
397
395
|
}
|
|
396
|
+
if (getArchivedTeams().has(params.name)) {
|
|
397
|
+
return {
|
|
398
|
+
content: [
|
|
399
|
+
{
|
|
400
|
+
type: "text",
|
|
401
|
+
text:
|
|
402
|
+
`Team "${params.name}" already exists as an archive. ` +
|
|
403
|
+
"Use team_resume to continue it or choose a new name.",
|
|
404
|
+
},
|
|
405
|
+
],
|
|
406
|
+
details: {},
|
|
407
|
+
isError: true,
|
|
408
|
+
};
|
|
409
|
+
}
|
|
398
410
|
createTeamStore(params.name);
|
|
399
411
|
appendDashboardFeedEvent(params.name, "orchestrator", "all", `Team "${params.name}" created`);
|
|
400
412
|
notifyDashboardChanged();
|
|
@@ -492,16 +504,27 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
492
504
|
description: [
|
|
493
505
|
"Spawn a teammate with their own agent session, shared task board access, and inter-agent messaging.",
|
|
494
506
|
"They get standard coding tools plus team coordination tools.",
|
|
507
|
+
"Optionally load a named agent template for prompt/frontmatter defaults.",
|
|
495
508
|
"After spawning, use team_send to give them initial instructions.",
|
|
496
509
|
].join(" "),
|
|
497
510
|
parameters: Type.Object({
|
|
511
|
+
agent: Type.Optional(
|
|
512
|
+
Type.String({
|
|
513
|
+
description: "Optional agent template name to load from user/project agent directories.",
|
|
514
|
+
})
|
|
515
|
+
),
|
|
498
516
|
team: Type.String({ description: "Team name" }),
|
|
499
517
|
name: Type.String({ description: "Teammate name (unique within team)" }),
|
|
500
|
-
role: Type.
|
|
518
|
+
role: Type.Optional(
|
|
519
|
+
Type.String({
|
|
520
|
+
description:
|
|
521
|
+
"Role/description (guides behavior). Optional when agent is provided; otherwise required.",
|
|
522
|
+
})
|
|
523
|
+
),
|
|
501
524
|
model: Type.Optional(
|
|
502
525
|
Type.String({
|
|
503
526
|
description:
|
|
504
|
-
"Explicit model ID (fuzzy matched). When omitted, auto-routes based on role complexity.",
|
|
527
|
+
"Explicit model ID (fuzzy matched). When omitted, auto-routes based on role complexity or agent template.",
|
|
505
528
|
})
|
|
506
529
|
),
|
|
507
530
|
modelScope: Type.Optional(
|
|
@@ -510,10 +533,16 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
510
533
|
'Constrain auto-routing to a model family (e.g. "codex", "gemini"). Ignored when explicit model is set.',
|
|
511
534
|
})
|
|
512
535
|
),
|
|
536
|
+
thinkingLevel: Type.Optional(
|
|
537
|
+
Type.String({
|
|
538
|
+
description:
|
|
539
|
+
"Optional teammate thinking level: off, low, medium, or high. Defaults to the parent session when available.",
|
|
540
|
+
})
|
|
541
|
+
),
|
|
513
542
|
tools: Type.Optional(
|
|
514
543
|
Type.Array(Type.String(), {
|
|
515
544
|
description:
|
|
516
|
-
"Standard tool names: read, bash, edit, write, grep, find, ls. Default: all coding tools.",
|
|
545
|
+
"Standard tool names: read, bash, edit, write, grep, find, ls. Default: all coding tools or the agent template allowlist.",
|
|
517
546
|
})
|
|
518
547
|
),
|
|
519
548
|
}),
|
|
@@ -538,19 +567,37 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
538
567
|
isError: true,
|
|
539
568
|
};
|
|
540
569
|
}
|
|
570
|
+
if (!params.role && !params.agent) {
|
|
571
|
+
return {
|
|
572
|
+
content: [
|
|
573
|
+
{
|
|
574
|
+
type: "text",
|
|
575
|
+
text: "team_spawn requires either a role or an agent template.",
|
|
576
|
+
},
|
|
577
|
+
],
|
|
578
|
+
details: {},
|
|
579
|
+
isError: true,
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const inheritedThinkingLevel = (
|
|
584
|
+
ctx as { getThinkingLevel?: () => string | undefined } | undefined
|
|
585
|
+
)?.getThinkingLevel?.();
|
|
541
586
|
|
|
542
587
|
try {
|
|
543
|
-
const mate = await spawnTeammateSession(
|
|
588
|
+
const mate = await spawnTeammateSession({
|
|
589
|
+
agentName: params.agent,
|
|
544
590
|
cwd,
|
|
591
|
+
hints: params.modelScope ? { modelScope: params.modelScope } : undefined,
|
|
592
|
+
modelOverride: params.model,
|
|
593
|
+
name: params.name,
|
|
594
|
+
parentModelId: ctx?.model?.id,
|
|
595
|
+
piEvents: pi.events,
|
|
596
|
+
role: params.role,
|
|
545
597
|
team,
|
|
546
|
-
params.
|
|
547
|
-
params.
|
|
548
|
-
|
|
549
|
-
params.tools,
|
|
550
|
-
pi.events,
|
|
551
|
-
params.modelScope ? { modelScope: params.modelScope } : undefined,
|
|
552
|
-
ctx?.model?.id
|
|
553
|
-
);
|
|
598
|
+
thinkingLevel: params.thinkingLevel ?? inheritedThinkingLevel,
|
|
599
|
+
toolNames: params.tools,
|
|
600
|
+
});
|
|
554
601
|
mate.unsubscribe = bindDashboardSessionTracking(team.name, mate.name, mate.session);
|
|
555
602
|
const dashboardActivity = getDashboardActivity();
|
|
556
603
|
dashboardActivity.touch(team.name, mate.name);
|
|
@@ -801,23 +848,9 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
801
848
|
};
|
|
802
849
|
}
|
|
803
850
|
|
|
804
|
-
|
|
805
|
-
for (const [, mate] of team.teammates) {
|
|
806
|
-
try {
|
|
807
|
-
if (mate.session.isStreaming) await mate.session.abort();
|
|
808
|
-
mate.unsubscribe?.();
|
|
809
|
-
mate.session.dispose();
|
|
810
|
-
mate.status = "shutdown";
|
|
811
|
-
count++;
|
|
812
|
-
} catch (err) {
|
|
813
|
-
console.error(`Failed to clean up teammate ${mate.name}: ${err}`);
|
|
814
|
-
mate.status = "shutdown";
|
|
815
|
-
count++;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
|
|
851
|
+
const count = await disposeTeamSessions(team, true);
|
|
819
852
|
removeTeamView(params.team);
|
|
820
|
-
|
|
853
|
+
archiveRuntimeTeam(params.team);
|
|
821
854
|
return {
|
|
822
855
|
content: [
|
|
823
856
|
{
|
|
@@ -857,6 +890,8 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
857
890
|
),
|
|
858
891
|
}),
|
|
859
892
|
async execute(_toolCallId, params) {
|
|
893
|
+
refreshArchivedTeamStore();
|
|
894
|
+
|
|
860
895
|
// List mode — show all archived teams
|
|
861
896
|
if (!params.team) {
|
|
862
897
|
const archives = getArchivedTeams();
|
|
@@ -917,6 +952,12 @@ export function registerTeamsToolExtension(pi: ExtensionAPI): void {
|
|
|
917
952
|
}
|
|
918
953
|
}
|
|
919
954
|
|
|
955
|
+
try {
|
|
956
|
+
deleteArchivedTeamFromDisk(params.team);
|
|
957
|
+
} catch (error) {
|
|
958
|
+
console.error(`Failed to delete archived team ${params.team}: ${error}`);
|
|
959
|
+
}
|
|
960
|
+
refreshArchivedTeamStore();
|
|
920
961
|
notifyDashboardChanged();
|
|
921
962
|
return {
|
|
922
963
|
content: [
|
|
@@ -9,7 +9,14 @@ import { getIcon } from "../../_icons/index.js";
|
|
|
9
9
|
import { appendDashboardFeedEvent, refreshTeamView } from "../dashboard/state.js";
|
|
10
10
|
import { autoDispatch, wakeTeammate } from "../dispatch/auto-dispatch.js";
|
|
11
11
|
import type { Teammate } from "../state/types.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
addTeamMessage,
|
|
14
|
+
getUnread,
|
|
15
|
+
isTaskReady,
|
|
16
|
+
markRead,
|
|
17
|
+
type Team,
|
|
18
|
+
type TeamTask,
|
|
19
|
+
} from "../store.js";
|
|
13
20
|
|
|
14
21
|
/**
|
|
15
22
|
* Create the team coordination tools for a specific teammate.
|
|
@@ -24,6 +31,23 @@ export function createTeammateTools(
|
|
|
24
31
|
myName: string,
|
|
25
32
|
piEvents?: ExtensionAPI["events"]
|
|
26
33
|
): ToolDefinition[] {
|
|
34
|
+
/**
|
|
35
|
+
* Validate that the current teammate owns a claimed task before mutating it.
|
|
36
|
+
*
|
|
37
|
+
* @param action - Mutation being attempted (`complete` or `fail`)
|
|
38
|
+
* @param task - Task being mutated
|
|
39
|
+
* @returns Error text when the mutation should be rejected, otherwise null
|
|
40
|
+
*/
|
|
41
|
+
function getTaskOwnershipError(action: "complete" | "fail", task: TeamTask): string | null {
|
|
42
|
+
if (task.status !== "claimed") {
|
|
43
|
+
return `Task #${task.id} is ${task.status}. Claim it before trying to ${action} it.`;
|
|
44
|
+
}
|
|
45
|
+
if (task.assignee !== myName) {
|
|
46
|
+
return `Task #${task.id} is assigned to ${task.assignee ?? "(unassigned)"}. Only the assignee can ${action} it.`;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
27
51
|
const tasksTool: ToolDefinition = {
|
|
28
52
|
name: "team_tasks",
|
|
29
53
|
label: "team_tasks",
|
|
@@ -108,6 +132,15 @@ export function createTeammateTools(
|
|
|
108
132
|
}
|
|
109
133
|
|
|
110
134
|
if (params.action === "complete") {
|
|
135
|
+
const ownershipError = getTaskOwnershipError("complete", task);
|
|
136
|
+
if (ownershipError) {
|
|
137
|
+
return {
|
|
138
|
+
content: [{ type: "text" as const, text: ownershipError }],
|
|
139
|
+
details: {},
|
|
140
|
+
isError: true,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
111
144
|
task.status = "completed";
|
|
112
145
|
task.result = params.result || "(completed)";
|
|
113
146
|
piEvents?.emit("task_completed", {
|
|
@@ -129,6 +162,15 @@ export function createTeammateTools(
|
|
|
129
162
|
}
|
|
130
163
|
|
|
131
164
|
if (params.action === "fail") {
|
|
165
|
+
const ownershipError = getTaskOwnershipError("fail", task);
|
|
166
|
+
if (ownershipError) {
|
|
167
|
+
return {
|
|
168
|
+
content: [{ type: "text" as const, text: ownershipError }],
|
|
169
|
+
details: {},
|
|
170
|
+
isError: true,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
132
174
|
task.status = "failed";
|
|
133
175
|
task.result = params.result || "(failed)";
|
|
134
176
|
refreshTeamView(team as Team<Teammate>);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"keys.d.ts","sourceRoot":"","sources":["../src/keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -483,6 +483,38 @@ function matchesModifyOtherKeys(data, expectedKeycode, expectedModifier) {
|
|
|
483
483
|
const actualMod = modValue - 1;
|
|
484
484
|
return keycode === expectedKeycode && actualMod === expectedModifier;
|
|
485
485
|
}
|
|
486
|
+
/**
|
|
487
|
+
* Detect Windows Terminal by session environment variable.
|
|
488
|
+
* Excludes SSH sessions where WT_SESSION may leak from the host.
|
|
489
|
+
*
|
|
490
|
+
* @returns true when running directly inside Windows Terminal
|
|
491
|
+
*/
|
|
492
|
+
function isWindowsTerminalSession() {
|
|
493
|
+
return (Boolean(process.env.WT_SESSION) &&
|
|
494
|
+
!process.env.SSH_CONNECTION &&
|
|
495
|
+
!process.env.SSH_CLIENT &&
|
|
496
|
+
!process.env.SSH_TTY);
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Raw 0x08 (BS) is ambiguous in legacy terminals.
|
|
500
|
+
*
|
|
501
|
+
* - Windows Terminal uses it for Ctrl+Backspace.
|
|
502
|
+
* - Some legacy terminals and tmux setups send it for plain Backspace.
|
|
503
|
+
*
|
|
504
|
+
* Prefer explicit Kitty / CSI-u / modifyOtherKeys sequences whenever they are
|
|
505
|
+
* available. Fall back to a Windows Terminal heuristic only for raw BS bytes.
|
|
506
|
+
*
|
|
507
|
+
* @param data - Raw terminal input byte
|
|
508
|
+
* @param expectedModifier - Modifier bitmask to match against
|
|
509
|
+
* @returns true if the data matches backspace with the given modifier
|
|
510
|
+
*/
|
|
511
|
+
function matchesRawBackspace(data, expectedModifier) {
|
|
512
|
+
if (data === "\x7f")
|
|
513
|
+
return expectedModifier === 0;
|
|
514
|
+
if (data !== "\x08")
|
|
515
|
+
return false;
|
|
516
|
+
return isWindowsTerminalSession() ? expectedModifier === MODIFIERS.ctrl : expectedModifier === 0;
|
|
517
|
+
}
|
|
486
518
|
// =============================================================================
|
|
487
519
|
// Generic Key Matching
|
|
488
520
|
// =============================================================================
|
|
@@ -556,7 +588,9 @@ export function matchesKey(data, keyId) {
|
|
|
556
588
|
case "esc":
|
|
557
589
|
if (modifier !== 0)
|
|
558
590
|
return false;
|
|
559
|
-
return data === "\x1b" ||
|
|
591
|
+
return (data === "\x1b" ||
|
|
592
|
+
matchesKittySequence(data, CODEPOINTS.escape, 0) ||
|
|
593
|
+
matchesModifyOtherKeys(data, CODEPOINTS.escape, 0));
|
|
560
594
|
case "space":
|
|
561
595
|
if (!_kittyProtocolActive) {
|
|
562
596
|
if (ctrl && !alt && !shift && data === "\x00") {
|
|
@@ -567,9 +601,12 @@ export function matchesKey(data, keyId) {
|
|
|
567
601
|
}
|
|
568
602
|
}
|
|
569
603
|
if (modifier === 0) {
|
|
570
|
-
return data === " " ||
|
|
604
|
+
return (data === " " ||
|
|
605
|
+
matchesKittySequence(data, CODEPOINTS.space, 0) ||
|
|
606
|
+
matchesModifyOtherKeys(data, CODEPOINTS.space, 0));
|
|
571
607
|
}
|
|
572
|
-
return matchesKittySequence(data, CODEPOINTS.space, modifier)
|
|
608
|
+
return (matchesKittySequence(data, CODEPOINTS.space, modifier) ||
|
|
609
|
+
matchesModifyOtherKeys(data, CODEPOINTS.space, modifier));
|
|
573
610
|
case "tab":
|
|
574
611
|
if (shift && !ctrl && !alt) {
|
|
575
612
|
return data === "\x1b[Z" || matchesKittySequence(data, CODEPOINTS.tab, MODIFIERS.shift);
|
|
@@ -629,12 +666,25 @@ export function matchesKey(data, keyId) {
|
|
|
629
666
|
if (data === "\x1b\x7f" || data === "\x1b\b") {
|
|
630
667
|
return true;
|
|
631
668
|
}
|
|
632
|
-
return matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt)
|
|
669
|
+
return (matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.alt) ||
|
|
670
|
+
matchesModifyOtherKeys(data, CODEPOINTS.backspace, MODIFIERS.alt));
|
|
671
|
+
}
|
|
672
|
+
if (ctrl && !alt && !shift) {
|
|
673
|
+
// Legacy raw 0x08 is ambiguous: it can be Ctrl+Backspace on Windows
|
|
674
|
+
// Terminal or plain Backspace on other terminals, while also
|
|
675
|
+
// overlapping with Ctrl+H.
|
|
676
|
+
if (matchesRawBackspace(data, MODIFIERS.ctrl))
|
|
677
|
+
return true;
|
|
678
|
+
return (matchesKittySequence(data, CODEPOINTS.backspace, MODIFIERS.ctrl) ||
|
|
679
|
+
matchesModifyOtherKeys(data, CODEPOINTS.backspace, MODIFIERS.ctrl));
|
|
633
680
|
}
|
|
634
681
|
if (modifier === 0) {
|
|
635
|
-
return (
|
|
682
|
+
return (matchesRawBackspace(data, 0) ||
|
|
683
|
+
matchesKittySequence(data, CODEPOINTS.backspace, 0) ||
|
|
684
|
+
matchesModifyOtherKeys(data, CODEPOINTS.backspace, 0));
|
|
636
685
|
}
|
|
637
|
-
return matchesKittySequence(data, CODEPOINTS.backspace, modifier)
|
|
686
|
+
return (matchesKittySequence(data, CODEPOINTS.backspace, modifier) ||
|
|
687
|
+
matchesModifyOtherKeys(data, CODEPOINTS.backspace, modifier));
|
|
638
688
|
case "insert":
|
|
639
689
|
if (modifier === 0) {
|
|
640
690
|
return (matchesLegacySequence(data, LEGACY_KEY_SEQUENCES.insert) ||
|
|
@@ -914,8 +964,10 @@ export function parseKey(data) {
|
|
|
914
964
|
return "ctrl+space";
|
|
915
965
|
if (data === " ")
|
|
916
966
|
return "space";
|
|
917
|
-
if (data === "\x7f"
|
|
967
|
+
if (data === "\x7f")
|
|
918
968
|
return "backspace";
|
|
969
|
+
if (data === "\x08")
|
|
970
|
+
return isWindowsTerminalSession() ? "ctrl+backspace" : "backspace";
|
|
919
971
|
if (data === "\x1b[Z")
|
|
920
972
|
return "shift+tab";
|
|
921
973
|
if (!_kittyProtocolActive && data === "\x1b\r")
|