@gajae-code/coding-agent 0.10.0 → 0.10.2

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 (65) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/types/cli.d.ts +1 -0
  3. package/dist/types/config/settings.d.ts +2 -0
  4. package/dist/types/coordinator/contract.d.ts +1 -1
  5. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  6. package/dist/types/coordinator-mcp/server.d.ts +17 -5
  7. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  8. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  9. package/dist/types/modes/components/gajae-pet-widget.d.ts +1 -0
  10. package/dist/types/modes/components/pet-capability.d.ts +37 -0
  11. package/dist/types/modes/components/pet-selector.d.ts +1 -1
  12. package/dist/types/modes/components/settings-selector.d.ts +9 -0
  13. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  14. package/dist/types/modes/components/status-line.d.ts +0 -10
  15. package/dist/types/modes/interactive-mode.d.ts +2 -1
  16. package/dist/types/modes/print-mode.d.ts +3 -4
  17. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  18. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  19. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  20. package/dist/types/modes/types.d.ts +11 -1
  21. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  22. package/dist/types/session/agent-session.d.ts +14 -1
  23. package/dist/types/session/context-estimation.d.ts +31 -0
  24. package/package.json +7 -7
  25. package/src/cli.ts +1 -0
  26. package/src/config/model-profiles.ts +19 -19
  27. package/src/config/settings.ts +96 -45
  28. package/src/coordinator/contract.ts +1 -0
  29. package/src/coordinator-mcp/policy.ts +30 -0
  30. package/src/coordinator-mcp/server.ts +294 -66
  31. package/src/coordinator-mcp/session-reaper.ts +129 -0
  32. package/src/extensibility/extensions/runner.ts +1 -1
  33. package/src/extensibility/extensions/types.ts +3 -1
  34. package/src/extensibility/slash-commands.ts +3 -3
  35. package/src/gjc-runtime/tmux-owner-isolation.ts +4 -1
  36. package/src/internal-urls/docs-index.generated.ts +7 -7
  37. package/src/main.ts +3 -5
  38. package/src/modes/acp/acp-agent.ts +2 -2
  39. package/src/modes/components/custom-editor.ts +1 -0
  40. package/src/modes/components/footer.ts +3 -3
  41. package/src/modes/components/gajae-pet-widget.ts +169 -20
  42. package/src/modes/components/pet-capability.ts +115 -0
  43. package/src/modes/components/pet-selector.ts +4 -6
  44. package/src/modes/components/settings-selector.ts +28 -2
  45. package/src/modes/components/status-line/segments.ts +13 -6
  46. package/src/modes/components/status-line/types.ts +1 -1
  47. package/src/modes/components/status-line.ts +4 -171
  48. package/src/modes/controllers/extension-ui-controller.ts +10 -9
  49. package/src/modes/controllers/input-controller.ts +43 -13
  50. package/src/modes/controllers/selector-controller.ts +7 -18
  51. package/src/modes/interactive-mode.ts +65 -4
  52. package/src/modes/print-mode.ts +228 -92
  53. package/src/modes/rpc/rpc-client.ts +58 -1
  54. package/src/modes/rpc/rpc-mode.ts +286 -85
  55. package/src/modes/rpc/rpc-types.ts +15 -1
  56. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  57. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  58. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  59. package/src/modes/types.ts +11 -1
  60. package/src/modes/utils/context-usage.ts +56 -92
  61. package/src/session/agent-session.ts +337 -90
  62. package/src/session/context-estimation.ts +91 -0
  63. package/src/session/session-manager.ts +9 -2
  64. package/src/slash-commands/builtin-registry.ts +31 -19
  65. package/src/slash-commands/helpers/context-report.ts +26 -4
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Session-neutral fixed-context (non-message) token estimation.
3
+ *
4
+ * Lives in the session layer so load-bearing session/compaction behavior
5
+ * (`AgentSession` context estimates) never depends on a presentation module.
6
+ * UI surfaces (`modes/utils/context-usage.ts`, status line) import from here
7
+ * as well, so every surface reports the same numbers.
8
+ */
9
+ import { estimateTextTokensHeuristic } from "@gajae-code/agent-core/compaction";
10
+ import type { Skill } from "../extensibility/skills";
11
+ import type { Tool } from "../tools";
12
+ import type { AgentSession } from "./agent-session";
13
+
14
+ export function estimateSkillsTokens(skills: readonly Skill[]): number {
15
+ const fragments: string[] = [];
16
+ for (const skill of skills) {
17
+ // "- name: description\n" wire framing tokenizes ~identically to the
18
+ // concatenated form, so encode each piece separately and sum.
19
+ fragments.push(skill.name, skill.description);
20
+ }
21
+ return estimateTextTokensHeuristic(fragments);
22
+ }
23
+
24
+ export function estimateToolSchemaTokens(
25
+ tools: ReadonlyArray<Pick<Tool, "name" | "description" | "parameters">>,
26
+ ): number {
27
+ const fragments: string[] = [];
28
+ for (const tool of tools) {
29
+ fragments.push(tool.name, tool.description);
30
+ try {
31
+ fragments.push(JSON.stringify(tool.parameters ?? {}));
32
+ } catch {
33
+ // Schema may contain functions or cycles; ignore.
34
+ }
35
+ }
36
+ return estimateTextTokensHeuristic(fragments);
37
+ }
38
+
39
+ /**
40
+ * Compute just the NON-MESSAGE token total: system prompt (with its skills
41
+ * section subtracted, since skills are tokenized separately) + system context
42
+ * (the rest of the system-prompt array) + tools + skills.
43
+ *
44
+ * Exposed so callers like `StatusLineComponent` can cache the non-message
45
+ * total separately from the message total. Non-message inputs (skills,
46
+ * tools, system prompt) change rarely; the message list grows on every
47
+ * streaming turn. Splitting the two lets the caller refresh each on its own
48
+ * cadence — non-message recomputed only when the inputs identity changes,
49
+ * messages walked incrementally as new entries append.
50
+ */
51
+ export function computeNonMessageTokens(session: AgentSession): number {
52
+ const parts = computeNonMessageBreakdown(session);
53
+ return (
54
+ parts.systemPromptTokens + parts.systemContextTokens + parts.rulesTokens + parts.toolsTokens + parts.skillsTokens
55
+ );
56
+ }
57
+
58
+ /**
59
+ * Shared helper for the four non-message token totals. Single source of truth
60
+ * for `computeNonMessageTokens` (session estimates + status-line incremental
61
+ * cache) and `computeContextBreakdown` (/context panel). The split avoids
62
+ * drift between the surfaces — they MUST report the same numbers.
63
+ */
64
+ export function computeNonMessageBreakdown(session: AgentSession): {
65
+ rulesTokens: number;
66
+ skillsTokens: number;
67
+ toolsTokens: number;
68
+ systemContextTokens: number;
69
+ systemPromptTokens: number;
70
+ } {
71
+ const skillsTokens = estimateSkillsTokens(session.skills ?? []);
72
+ const toolsTokens = estimateToolSchemaTokens(session.agent?.state?.tools ?? []);
73
+ const systemPromptParts = session.systemPrompt ?? [];
74
+ const rulesTokens = estimateRulesTokens(systemPromptParts);
75
+ const systemContextTokens = estimateTextTokensHeuristic(systemPromptParts.slice(1));
76
+ const systemPromptTokens = Math.max(
77
+ 0,
78
+ estimateTextTokensHeuristic(systemPromptParts[0] ?? "") - skillsTokens - rulesTokens,
79
+ );
80
+ return { rulesTokens, skillsTokens, toolsTokens, systemContextTokens, systemPromptTokens };
81
+ }
82
+
83
+ function estimateRulesTokens(systemPromptParts: readonly string[]): number {
84
+ const fragments: string[] = [];
85
+ for (const part of systemPromptParts) {
86
+ for (const match of part.matchAll(/<rules>[\s\S]*?<\/rules>/g)) {
87
+ fragments.push(match[0]);
88
+ }
89
+ }
90
+ return fragments.length === 0 ? 0 : estimateTextTokensHeuristic(fragments);
91
+ }
@@ -3335,6 +3335,11 @@ export class SessionManager {
3335
3335
 
3336
3336
  restoreState(snapshot: SessionManagerStateSnapshot): void {
3337
3337
  const restoredFileEntries = [...snapshot.materializedFileEntries];
3338
+ const retainsPersistWriter =
3339
+ this.#persistWriter?.isOpen() === true &&
3340
+ this.#sessionId === snapshot.sessionId &&
3341
+ this.#sessionFile === snapshot.sessionFile &&
3342
+ this.#persistWriterPath === snapshot.sessionFile;
3338
3343
  this.#sessionId = snapshot.sessionId;
3339
3344
  this.#sessionName = snapshot.sessionName;
3340
3345
  this.#titleSource = snapshot.titleSource;
@@ -3342,8 +3347,10 @@ export class SessionManager {
3342
3347
  this.#flushed = snapshot.flushed;
3343
3348
  this.#needsFullRewriteOnNextPersist = snapshot.needsFullRewriteOnNextPersist;
3344
3349
  this.#fileEntries = restoredFileEntries;
3345
- this.#persistWriter = undefined;
3346
- this.#persistWriterPath = undefined;
3350
+ if (!retainsPersistWriter) {
3351
+ this.#persistWriter = undefined;
3352
+ this.#persistWriterPath = undefined;
3353
+ }
3347
3354
  this.#persistChain = Promise.resolve();
3348
3355
  this.#persistError = undefined;
3349
3356
  this.#persistErrorReported = false;
@@ -3,7 +3,7 @@ import * as path from "node:path";
3
3
  import { ThinkingLevel } from "@gajae-code/agent-core";
4
4
  import { type Model, modelsAreEqual } from "@gajae-code/ai";
5
5
  import { getOAuthProviders } from "@gajae-code/ai/utils/oauth";
6
- import { isPetMode, PET_MODE_IDS, PET_SKIN_IDS, PET_SKINS, Spacer, Text } from "@gajae-code/tui";
6
+ import { PET_SKINS, type PetMode, Spacer, Text } from "@gajae-code/tui";
7
7
  import { setProjectDir } from "@gajae-code/utils";
8
8
  import { jobElapsedMs } from "../async";
9
9
  import { materializeActiveModelProfileAssignments } from "../config/model-profile-activation";
@@ -22,7 +22,6 @@ import {
22
22
  import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../discovery/helpers.js";
23
23
  import { resolveMemoryBackend } from "../memory-backend";
24
24
  import { DynamicBorder } from "../modes/components/dynamic-border";
25
- import { GajaePetWidget } from "../modes/components/gajae-pet-widget";
26
25
  import { theme } from "../modes/theme/theme";
27
26
  import type { InteractiveModeContext } from "../modes/types";
28
27
  import {
@@ -65,6 +64,22 @@ export type { BuiltinSlashCommand, SubcommandDef } from "./types";
65
64
  /** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
66
65
  export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
67
66
 
67
+ const PET_COMMAND_OPTIONS: ReadonlyArray<{ name: string; mode: PetMode; description: string }> = [
68
+ { name: "off", mode: "off", description: "Hide the pet" },
69
+ { name: "RedGajae", mode: "red", description: PET_SKINS.red.description },
70
+ { name: "BlueGajae", mode: "blue", description: PET_SKINS.blue.description },
71
+ ];
72
+ const PET_COMMAND_HINT = `[${PET_COMMAND_OPTIONS.map(option => option.name).join("|")}]`;
73
+ /**
74
+ * Deprecated inputs kept accepted for compatibility (`/pet on|red|blue`).
75
+ * Display, completion, and inline hints stay canonical (`PET_COMMAND_OPTIONS`).
76
+ */
77
+ const PET_COMMAND_DEPRECATED_INPUTS: Readonly<Record<string, PetMode>> = {
78
+ on: "red",
79
+ red: "red",
80
+ blue: "blue",
81
+ };
82
+
68
83
  type GjcModelBatchAssignmentTargetId = "all-role-agents" | "all-targets";
69
84
  type ParsedModelCommandArgs =
70
85
  | { kind: "summary" }
@@ -562,32 +577,29 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
562
577
  {
563
578
  name: "pet",
564
579
  description: "Gajae pet living beside the composer",
565
- subcommands: [
566
- { name: "off", description: "Hide the pet" },
567
- ...PET_SKIN_IDS.map(id => ({ name: id, description: PET_SKINS[id].description })),
568
- ],
569
- inlineHint: `[${PET_MODE_IDS.join("|")}]`,
580
+ subcommands: PET_COMMAND_OPTIONS.map(option => ({ name: option.name, description: option.description })),
581
+ inlineHint: PET_COMMAND_HINT,
570
582
  allowArgs: true,
571
583
  handleTui: (command, runtime) => {
572
584
  const ctx = runtime.ctx;
573
585
  const raw = command.args?.trim().toLowerCase() ?? "";
574
- const arg = raw === "on" ? "red" : raw;
575
- if (!arg) {
586
+ const arg =
587
+ PET_COMMAND_OPTIONS.find(option => option.name.toLowerCase() === raw)?.mode ??
588
+ PET_COMMAND_DEPRECATED_INPUTS[raw];
589
+ if (!raw) {
576
590
  ctx.showPetSelector();
577
591
  ctx.editor.setText("");
578
592
  return;
579
593
  }
580
- if (arg !== "off" && !GajaePetWidget.pixelProtocol()) {
581
- ctx.showStatus(
582
- "Gajae pet needs a sixel/kitty-graphics terminal (Windows Terminal 1.22+, kitty, Ghostty, WezTerm)",
583
- { dim: true },
584
- );
585
- } else if (isPetMode(arg)) {
586
- ctx.setPetMode(arg);
587
- const name = arg === "off" ? "Gajae pet hidden" : `${PET_SKINS[arg].label} is here`;
588
- ctx.showStatus(name);
594
+ if (arg) {
595
+ // The shared commit policy rechecks capability, persists only on
596
+ // acceptance, and surfaces the actionable warning on rejection.
597
+ if (ctx.setPetMode(arg)) {
598
+ const name = arg === "off" ? "Gajae pet hidden" : `${PET_SKINS[arg].label} is here`;
599
+ ctx.showStatus(name);
600
+ }
589
601
  } else {
590
- ctx.showStatus(`Usage: /pet [${PET_MODE_IDS.join("|")}]`, { dim: true });
602
+ ctx.showStatus(`Usage: /pet ${PET_COMMAND_HINT}`, { dim: true });
591
603
  }
592
604
  ctx.editor.setText("");
593
605
  },
@@ -83,6 +83,12 @@ function formatReserveText(runtime: SlashCommandRuntime, contextWindow: number,
83
83
  return "unknown";
84
84
  }
85
85
 
86
+ function contextUsageSourceLabel(source: "provider_anchor" | "heuristic" | "unknown"): string {
87
+ if (source === "provider_anchor") return "provider-reported";
88
+ if (source === "unknown") return "estimated; exact count unknown until next response";
89
+ return "estimated";
90
+ }
91
+
86
92
  /**
87
93
  * Build the `/context` ACP-mode text. Tries the rich breakdown first
88
94
  * (categories + auto-compact buffer + free slack) and falls back to the
@@ -96,11 +102,21 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
96
102
  return "Context usage is unavailable: no model is selected for this session.";
97
103
  }
98
104
  const promptUsage = findLastAssistantUsage(history.activeMessages);
99
- const usedPct = (breakdown.usedTokens / breakdown.contextWindow) * 100;
105
+ const activeContext =
106
+ breakdown.usedTokens === null
107
+ ? `Active context: unknown / ${breakdown.contextWindow.toLocaleString()} tokens (exact count unknown until next response) (${contextUsageSourceLabel(breakdown.source)})`
108
+ : `Active context: ${breakdown.usedTokens.toLocaleString()} / ${breakdown.contextWindow.toLocaleString()} tokens (${((breakdown.usedTokens / breakdown.contextWindow) * 100).toFixed(1)}% used) (${contextUsageSourceLabel(breakdown.source)})`;
100
109
  const lines = [
101
110
  "Context usage",
102
111
  `Model: ${breakdown.model?.provider ?? "unknown"}/${breakdown.model?.id ?? "unknown"}`,
103
- `Active context: ${breakdown.usedTokens.toLocaleString()} / ${breakdown.contextWindow.toLocaleString()} tokens (${usedPct.toFixed(1)}% used)`,
112
+ activeContext,
113
+ ...(breakdown.source !== "unknown" &&
114
+ breakdown.usedTokens !== null &&
115
+ breakdown.estimatedCategoryTotal !== breakdown.usedTokens
116
+ ? [
117
+ `Estimated category total: ${breakdown.estimatedCategoryTotal.toLocaleString()} tokens (composition below is estimated)`,
118
+ ]
119
+ : []),
104
120
  `Reserve: ${formatReserveText(runtime, breakdown.contextWindow, breakdown.autoCompactBufferTokens)}`,
105
121
  "",
106
122
  "Active context breakdown (estimated)",
@@ -111,7 +127,13 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
111
127
  if (breakdown.autoCompactBufferTokens > 0) {
112
128
  lines.push(formatTokenLine("Reserve", breakdown.autoCompactBufferTokens, breakdown.contextWindow));
113
129
  }
114
- lines.push(formatTokenLine("Free", breakdown.freeTokens, breakdown.contextWindow));
130
+ lines.push(
131
+ formatTokenLine(
132
+ breakdown.usedTokens === null ? "Free (estimated)" : "Free",
133
+ breakdown.freeTokens,
134
+ breakdown.contextWindow,
135
+ ),
136
+ );
115
137
  lines.push(
116
138
  "",
117
139
  "History",
@@ -141,7 +163,7 @@ export function buildContextReportText(runtime: SlashCommandRuntime): string {
141
163
  if (!fallback) return "Context usage is unavailable.";
142
164
  return [
143
165
  "Context usage",
144
- `Active context: ${fallback.tokens === null || fallback.tokens === undefined ? "unknown" : fallback.tokens.toLocaleString()}`,
166
+ `Active context: ${fallback.tokens === null ? "unknown" : fallback.tokens.toLocaleString()} (${contextUsageSourceLabel(fallback.source)})`,
145
167
  `Context window: ${fallback.contextWindow.toLocaleString()}`,
146
168
  "Breakdown: unknown",
147
169
  ].join("\n");