@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.
Files changed (60) hide show
  1. package/dist/auth-hardening.d.ts +12 -0
  2. package/dist/auth-hardening.d.ts.map +1 -1
  3. package/dist/auth-hardening.js +30 -7
  4. package/dist/auth-hardening.js.map +1 -1
  5. package/dist/cli.js +5 -0
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +1 -1
  8. package/dist/config.js +1 -1
  9. package/dist/install.js +2 -2
  10. package/dist/install.js.map +1 -1
  11. package/dist/interactive-mode-patch.d.ts.map +1 -1
  12. package/dist/interactive-mode-patch.js +119 -7
  13. package/dist/interactive-mode-patch.js.map +1 -1
  14. package/dist/model-metadata-overrides.d.ts +19 -0
  15. package/dist/model-metadata-overrides.d.ts.map +1 -0
  16. package/dist/model-metadata-overrides.js +38 -0
  17. package/dist/model-metadata-overrides.js.map +1 -0
  18. package/dist/sdk.d.ts +2 -0
  19. package/dist/sdk.d.ts.map +1 -1
  20. package/dist/sdk.js +28 -1
  21. package/dist/sdk.js.map +1 -1
  22. package/extensions/__integration__/teams-runtime.test.ts +22 -1
  23. package/extensions/_shared/__tests__/shell-policy.test.ts +197 -0
  24. package/extensions/_shared/shell-policy.ts +27 -0
  25. package/extensions/background-task-tool/index.ts +2 -1
  26. package/extensions/bash-tool-enhanced/index.ts +2 -1
  27. package/extensions/custom-footer/__tests__/index.test.ts +29 -0
  28. package/extensions/custom-footer/context-display.ts +49 -0
  29. package/extensions/custom-footer/index.ts +10 -23
  30. package/extensions/permissions/index.ts +31 -10
  31. package/extensions/plan-mode-tool/__tests__/index.test.ts +32 -2
  32. package/extensions/plan-mode-tool/index.ts +6 -1
  33. package/extensions/skill-commands/__tests__/shared-skills-dirs.test.ts +113 -0
  34. package/extensions/skill-commands/index.ts +62 -5
  35. package/extensions/slash-command-bridge/index.ts +30 -1
  36. package/extensions/subagent-tool/__tests__/process-liveness.test.ts +42 -3
  37. package/extensions/subagent-tool/process.ts +132 -21
  38. package/extensions/tasks/__tests__/store.test.ts +26 -2
  39. package/extensions/tasks/commands/register-tasks-extension.ts +2 -2
  40. package/extensions/tasks/index.ts +5 -5
  41. package/extensions/tasks/state/index.ts +90 -36
  42. package/extensions/teams-tool/__tests__/archive-store.test.ts +98 -0
  43. package/extensions/teams-tool/__tests__/peer-messaging.test.ts +26 -0
  44. package/extensions/teams-tool/archive-store.ts +200 -0
  45. package/extensions/teams-tool/sessions/spawn.ts +244 -71
  46. package/extensions/teams-tool/tools/register-extension.ts +146 -105
  47. package/extensions/teams-tool/tools/teammate-tools.ts +43 -1
  48. package/node_modules/@mariozechner/pi-tui/dist/keys.d.ts.map +1 -1
  49. package/node_modules/@mariozechner/pi-tui/dist/keys.js +59 -7
  50. package/node_modules/@mariozechner/pi-tui/dist/keys.js.map +1 -1
  51. package/node_modules/@mariozechner/pi-tui/package.json +1 -1
  52. package/node_modules/@mariozechner/pi-tui/src/keys.ts +71 -7
  53. package/package.json +5 -5
  54. package/skills/tallow-expert/SKILL.md +1 -1
  55. package/templates/agents/architect.md +13 -5
  56. package/templates/agents/debug.md +3 -3
  57. package/templates/agents/explore.md +9 -2
  58. package/templates/agents/refactor.md +2 -2
  59. package/templates/agents/scout.md +3 -2
  60. 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
- * Abort all teammates that are currently streaming work.
158
- * @returns Number of teammates that received an abort request
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
- async function abortRunningTeammates(): Promise<number> {
161
- const running: Array<{ teammate: Teammate; team: Team<Teammate> }> = [];
162
- for (const [, team] of getTeams() as Map<string, Team<Teammate>>) {
163
- for (const [, teammate] of team.teammates) {
164
- if (teammate.status !== "working" && !teammate.session.isStreaming) continue;
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
- if (running.length === 0) return 0;
175
+ }
169
176
 
170
- await Promise.all(
171
- running.map(async ({ teammate }) => {
172
- try {
173
- await teammate.session.abort();
174
- } catch {
175
- // Best-effort abort.
176
- }
177
- })
178
- );
179
-
180
- const touchedTeams = new Set<Team<Teammate>>();
181
- const dashboardActivity = getDashboardActivity();
182
- for (const { teammate, team } of running) {
183
- if (teammate.status === "working") teammate.status = "idle";
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
- * Handle Esc inside dashboard: cancel active work first, then close dashboard.
195
- * @param ctx - Extension context
196
- * @returns void
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 handleDashboardEscape(ctx: ExtensionContext): void {
199
- if (dashboardCancelInFlight) return;
200
- void (async () => {
201
- dashboardCancelInFlight = true;
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
- const cancelled = await abortRunningTeammates();
204
- if (cancelled > 0) {
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
- if (!dashboardEnabled) return;
212
- disableDashboard(ctx, false);
213
- ctx.ui.notify("Team dashboard disabled.", "info");
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
- dashboardCancelInFlight = false;
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
- for (const [, mate] of team.teammates) {
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
- archiveTeam(name);
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
- archiveTeam(name);
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.String({ description: "Role/description (guides their behavior)" }),
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.name,
547
- params.role,
548
- params.model,
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
- let count = 0;
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
- archiveTeam(params.team);
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 { addTeamMessage, getUnread, isTaskReady, markRead, type Team } from "../store.js";
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" || matchesKittySequence(data, CODEPOINTS.escape, 0);
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 === " " || matchesKittySequence(data, CODEPOINTS.space, 0);
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 (data === "\x7f" || data === "\x08" || matchesKittySequence(data, CODEPOINTS.backspace, 0));
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" || data === "\x08")
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")