@oh-my-pi/pi-coding-agent 16.0.10 → 16.1.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.
Files changed (135) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/cli.js +3344 -3371
  3. package/dist/types/advisor/index.d.ts +1 -0
  4. package/dist/types/advisor/transcript-recorder.d.ts +52 -0
  5. package/dist/types/commit/agentic/agent.d.ts +1 -1
  6. package/dist/types/config/settings-schema.d.ts +14 -8
  7. package/dist/types/edit/file-snapshot-store.d.ts +1 -1
  8. package/dist/types/extensibility/extensions/types.d.ts +7 -0
  9. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +1 -0
  10. package/dist/types/modes/components/agent-hub.d.ts +6 -1
  11. package/dist/types/modes/components/agent-transcript-viewer.d.ts +39 -0
  12. package/dist/types/modes/components/assistant-message.d.ts +8 -0
  13. package/dist/types/modes/components/cache-invalidation-marker.d.ts +34 -0
  14. package/dist/types/modes/components/chat-transcript-builder.d.ts +42 -0
  15. package/dist/types/modes/components/compaction-summary-message.d.ts +14 -1
  16. package/dist/types/modes/components/index.d.ts +0 -1
  17. package/dist/types/modes/components/message-frame.d.ts +6 -4
  18. package/dist/types/modes/controllers/command-controller.d.ts +3 -2
  19. package/dist/types/modes/interactive-mode.d.ts +4 -2
  20. package/dist/types/modes/theme/theme.d.ts +7 -1
  21. package/dist/types/modes/types.d.ts +9 -2
  22. package/dist/types/registry/agent-registry.d.ts +10 -3
  23. package/dist/types/sdk.d.ts +1 -1
  24. package/dist/types/session/agent-session.d.ts +20 -1
  25. package/dist/types/session/compact-modes.d.ts +60 -0
  26. package/dist/types/session/session-context.d.ts +7 -0
  27. package/dist/types/session/session-dump-format.d.ts +1 -0
  28. package/dist/types/session/streaming-output.d.ts +0 -2
  29. package/dist/types/session/tool-choice-queue.d.ts +14 -0
  30. package/dist/types/system-prompt.d.ts +3 -3
  31. package/dist/types/tools/__tests__/json-tree.test.d.ts +1 -0
  32. package/dist/types/tools/index.d.ts +4 -0
  33. package/dist/types/tools/resolve.d.ts +15 -5
  34. package/package.json +12 -12
  35. package/src/advisor/index.ts +1 -0
  36. package/src/advisor/transcript-recorder.ts +136 -0
  37. package/src/cli/stats-cli.ts +2 -11
  38. package/src/collab/host.ts +25 -13
  39. package/src/commit/agentic/agent.ts +2 -1
  40. package/src/commit/agentic/tools/git-file-diff.ts +2 -2
  41. package/src/commit/changelog/index.ts +1 -1
  42. package/src/commit/map-reduce/map-phase.ts +1 -1
  43. package/src/commit/map-reduce/utils.ts +1 -1
  44. package/src/config/settings-schema.ts +16 -9
  45. package/src/config/settings.ts +0 -6
  46. package/src/debug/log-viewer.ts +4 -4
  47. package/src/debug/raw-sse.ts +4 -4
  48. package/src/edit/file-snapshot-store.ts +1 -1
  49. package/src/edit/renderer.ts +9 -9
  50. package/src/eval/js/tool-bridge.ts +3 -2
  51. package/src/eval/py/prelude.py +3 -2
  52. package/src/export/html/tool-views.generated.js +28 -28
  53. package/src/extensibility/extensions/types.ts +7 -0
  54. package/src/hindsight/mental-models.ts +1 -1
  55. package/src/internal-urls/docs-index.generated.txt +1 -1
  56. package/src/internal-urls/history-protocol.ts +8 -3
  57. package/src/irc/bus.ts +8 -0
  58. package/src/lsp/index.ts +2 -2
  59. package/src/lsp/render.ts +7 -7
  60. package/src/main.ts +4 -1
  61. package/src/modes/acp/acp-agent.ts +63 -0
  62. package/src/modes/components/__tests__/skill-message.test.ts +92 -0
  63. package/src/modes/components/agent-dashboard.ts +1 -1
  64. package/src/modes/components/agent-hub.ts +97 -920
  65. package/src/modes/components/agent-transcript-viewer.ts +461 -0
  66. package/src/modes/components/assistant-message.ts +21 -0
  67. package/src/modes/components/cache-invalidation-marker.ts +84 -0
  68. package/src/modes/components/chat-transcript-builder.ts +476 -0
  69. package/src/modes/components/compaction-summary-message.ts +29 -1
  70. package/src/modes/components/custom-message.ts +4 -1
  71. package/src/modes/components/diff.ts +12 -35
  72. package/src/modes/components/dynamic-border.ts +1 -1
  73. package/src/modes/components/extensions/extension-dashboard.ts +1 -1
  74. package/src/modes/components/extensions/inspector-panel.ts +5 -5
  75. package/src/modes/components/hook-selector.ts +2 -2
  76. package/src/modes/components/index.ts +0 -1
  77. package/src/modes/components/message-frame.ts +10 -6
  78. package/src/modes/components/model-selector.ts +2 -2
  79. package/src/modes/components/overlay-box.ts +10 -9
  80. package/src/modes/components/skill-message.ts +39 -19
  81. package/src/modes/components/tiny-title-download-progress.ts +1 -1
  82. package/src/modes/components/welcome.ts +1 -1
  83. package/src/modes/controllers/command-controller.ts +12 -2
  84. package/src/modes/controllers/event-controller.ts +15 -1
  85. package/src/modes/controllers/input-controller.ts +8 -1
  86. package/src/modes/controllers/selector-controller.ts +11 -1
  87. package/src/modes/interactive-mode.ts +13 -3
  88. package/src/modes/theme/theme.ts +14 -0
  89. package/src/modes/types.ts +9 -2
  90. package/src/modes/utils/ui-helpers.ts +20 -2
  91. package/src/prompts/steering/user-interjection.md +3 -4
  92. package/src/prompts/tools/read.md +1 -1
  93. package/src/registry/agent-registry.ts +13 -4
  94. package/src/sdk.ts +9 -7
  95. package/src/session/agent-session.ts +182 -16
  96. package/src/session/compact-modes.ts +105 -0
  97. package/src/session/messages.ts +7 -9
  98. package/src/session/session-context.ts +54 -7
  99. package/src/session/session-dump-format.ts +4 -2
  100. package/src/session/session-history-format.ts +1 -1
  101. package/src/session/snapcompact-inline.ts +2 -2
  102. package/src/session/streaming-output.ts +5 -5
  103. package/src/session/tool-choice-queue.ts +59 -0
  104. package/src/slash-commands/builtin-registry.ts +16 -4
  105. package/src/system-prompt.ts +10 -9
  106. package/src/task/executor.ts +1 -1
  107. package/src/task/output-manager.ts +5 -0
  108. package/src/tools/__tests__/json-tree.test.ts +35 -0
  109. package/src/tools/approval.ts +1 -1
  110. package/src/tools/bash-interactive.ts +4 -4
  111. package/src/tools/bash.ts +0 -1
  112. package/src/tools/browser.ts +0 -1
  113. package/src/tools/eval.ts +1 -1
  114. package/src/tools/gh.ts +1 -1
  115. package/src/tools/index.ts +4 -0
  116. package/src/tools/irc.ts +1 -1
  117. package/src/tools/json-tree.ts +22 -5
  118. package/src/tools/read.ts +5 -6
  119. package/src/tools/resolve.ts +66 -41
  120. package/src/tui/output-block.ts +9 -9
  121. package/src/web/scrapers/firefox-addons.ts +1 -1
  122. package/src/web/scrapers/github.ts +1 -1
  123. package/src/web/scrapers/go-pkg.ts +2 -2
  124. package/src/web/scrapers/metacpan.ts +2 -2
  125. package/src/web/scrapers/nvd.ts +2 -2
  126. package/src/web/scrapers/ollama.ts +1 -1
  127. package/src/web/scrapers/opencorporates.ts +1 -1
  128. package/src/web/scrapers/pub-dev.ts +1 -1
  129. package/src/web/scrapers/repology.ts +1 -1
  130. package/src/web/scrapers/sourcegraph.ts +1 -1
  131. package/src/web/scrapers/terraform.ts +6 -6
  132. package/src/web/scrapers/wikidata.ts +2 -2
  133. package/src/workspace-tree.ts +1 -1
  134. package/dist/types/modes/components/branch-summary-message.d.ts +0 -13
  135. package/src/modes/components/branch-summary-message.ts +0 -46
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Manual `/compact` subcommands. Kept in a dependency-free leaf module so the
3
+ * slash-command registry, the interactive controllers, and `AgentSession`
4
+ * can all import the mode metadata + parser without pulling in the heavy
5
+ * `agent-session` module graph (which would form an import cycle through the
6
+ * slash-command registry) — same rationale as `shake-types.ts`.
7
+ *
8
+ * Each mode is a one-off override layered on top of the configured
9
+ * `compaction.*` settings for a single invocation; it never mutates settings.
10
+ * Adding a mode is a single entry here: the command surface (autocomplete +
11
+ * ACP hint), the parser, and the engine override all read this table.
12
+ */
13
+ /** Subcommand selecting a one-off compaction mode for manual `/compact`. */
14
+ export type CompactMode = "soft" | "remote" | "snapcompact";
15
+ /**
16
+ * Per-invocation overrides merged over the configured `compaction.*` settings.
17
+ * Narrowed to the two knobs the modes actually flip; the result stays
18
+ * assignable to the full `CompactionSettings`.
19
+ */
20
+ export interface CompactionOverride {
21
+ strategy?: "context-full" | "snapcompact";
22
+ remoteEnabled?: boolean;
23
+ }
24
+ export interface CompactModeDef {
25
+ readonly name: CompactMode;
26
+ /** One-line description surfaced in autocomplete + help. */
27
+ readonly description: string;
28
+ /** Settings overrides applied on top of `compaction.*` for this run. */
29
+ readonly overrides: CompactionOverride;
30
+ /**
31
+ * When true, the mode produces no LLM summary, so trailing focus text is
32
+ * meaningless and rejected by the parser (snapcompact archives history into
33
+ * images without a directed summary).
34
+ */
35
+ readonly rejectsFocus?: boolean;
36
+ /**
37
+ * When true, the mode explicitly demands a remote path; the engine warns and
38
+ * falls back to a local summary if neither a remote endpoint nor a
39
+ * provider-native compaction path is available.
40
+ */
41
+ readonly requiresRemote?: boolean;
42
+ }
43
+ export declare const COMPACT_MODES: readonly CompactModeDef[];
44
+ /** Resolve a subcommand token (case-insensitive) to its mode definition. */
45
+ export declare function findCompactMode(name: string): CompactModeDef | undefined;
46
+ /** Parsed `/compact` arguments: an optional mode plus optional focus text. */
47
+ export interface ParsedCompactArgs {
48
+ mode?: CompactMode;
49
+ instructions?: string;
50
+ }
51
+ /**
52
+ * Split `/compact` args into a leading mode subcommand + focus instructions.
53
+ *
54
+ * Backward compatible: when the first token is not a known mode, the entire
55
+ * argument string is treated as focus instructions (the historical behavior).
56
+ * A recognized mode with `rejectsFocus` and trailing text is an error.
57
+ */
58
+ export declare function parseCompactArgs(args: string): ParsedCompactArgs | {
59
+ error: string;
60
+ };
@@ -17,6 +17,13 @@ export interface SessionContext {
17
17
  mode: string;
18
18
  /** Mode-specific data from the last mode_change entry */
19
19
  modeData?: Record<string, unknown>;
20
+ /**
21
+ * Array parallel to messages, indicating which assistant turns should
22
+ * have their prompt-cache misses suppressed/explained (because a model,
23
+ * compaction, or plan-mode transition directly preceded them).
24
+ * Only populated in transcript mode.
25
+ */
26
+ cacheMissExplainedAt?: boolean[];
20
27
  }
21
28
  /** Lists session model strings to try when restoring, in fallback order. */
22
29
  export declare function getRestorableSessionModels(models: Readonly<Record<string, string>>, lastModelChangeRole: string | undefined): string[];
@@ -21,6 +21,7 @@ export interface FormatSessionDumpTextOptions {
21
21
  model?: Model | null;
22
22
  thinkingLevel?: ThinkingLevel | string | null;
23
23
  tools?: readonly SessionDumpToolInfo[];
24
+ inlineToolDescriptors?: boolean;
24
25
  }
25
26
  /**
26
27
  * Format messages and session metadata as markdown/plain text (same as
@@ -163,8 +163,6 @@ export declare function truncateMiddle(content: string, options?: TruncationOpti
163
163
  export interface InlineByteCapOptions {
164
164
  /** Inline byte budget. Defaults to {@link DEFAULT_MAX_BYTES}. */
165
165
  maxBytes?: number;
166
- /** What the text is, for the elision marker (e.g. "bash output"). */
167
- label: string;
168
166
  /**
169
167
  * Persist the full text as a session artifact. When an artifact id is
170
168
  * returned, a `[raw output: artifact://<id>]` footer is appended so the
@@ -67,6 +67,20 @@ export declare class ToolChoiceQueue {
67
67
  get hasInFlight(): boolean;
68
68
  /** Return the in-flight directive's onInvoked handler and mark it when called. */
69
69
  peekInFlightInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
70
+ /** Register (or replace by exact id) a non-forcing pending preview invoker. */
71
+ registerPendingInvoker(id: string, sourceToolName: string, onInvoked: (input: unknown) => Promise<unknown> | unknown): void;
72
+ /** Drop the pending invoker with this id (e.g. after it resolves). */
73
+ removePendingInvoker(id: string): void;
74
+ /** True when at least one non-forcing pending preview is registered. */
75
+ get hasPendingInvoker(): boolean;
76
+ /** The head (most-recently registered) pending invoker's handler, for resolve dispatch. */
77
+ peekPendingInvoker(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
78
+ /** The head pending preview's stable id + source tool, for building the agent-level
79
+ * SoftToolRequirement (the id drives reminder re-injection when the head changes). */
80
+ peekPendingHead(): {
81
+ id: string;
82
+ sourceToolName: string;
83
+ } | undefined;
70
84
  /** Remove all directives with the given label. Rejects in-flight if it matches. */
71
85
  removeByLabel(label: string): void;
72
86
  /** Empty the queue and reject any in-flight yield. */
@@ -54,11 +54,11 @@ export interface BuildSystemPromptOptions {
54
54
  toolNames?: string[];
55
55
  /** Text to append to system prompt. */
56
56
  appendSystemPrompt?: string;
57
- /** Repeat full tool descriptions in system prompt. Default: false */
58
- repeatToolDescriptions?: boolean;
57
+ /** Inline full tool descriptors in the system prompt. Default: true */
58
+ inlineToolDescriptors?: boolean;
59
59
  /**
60
60
  * Whether provider-native tool calling is active (no owned/in-band syntax).
61
- * When true and `repeatToolDescriptions` is false, the inventory renders as a
61
+ * When true and `inlineToolDescriptors` is false, the inventory renders as a
62
62
  * compact tool-name list; otherwise it renders full `# Tool:` sections. Default: true
63
63
  */
64
64
  nativeTools?: boolean;
@@ -0,0 +1 @@
1
+ export {};
@@ -272,6 +272,10 @@ export interface ToolSession {
272
272
  }): void;
273
273
  /** Peek the currently in-flight tool-choice queue directive's invocation handler. Used by the `resolve` tool to dispatch to the pending action. */
274
274
  peekQueueInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
275
+ /** Peek the most-recently registered non-forcing pending preview invoker. The `resolve`
276
+ * tool dispatches to it so a staged preview resolves WITHOUT forcing tool_choice — the
277
+ * agent-loop's SoftToolRequirement lifecycle owns reminder injection and escalation. */
278
+ peekPendingInvoker?(): ((input: unknown) => Promise<unknown> | unknown) | undefined;
275
279
  /** Peek the long-lived "standing" resolve handler registered by a mode (e.g. plan mode).
276
280
  * Consulted by the `resolve` tool as a fallback when no queue invoker is in flight,
277
281
  * letting modes accept `resolve` invocations without forcing the tool choice every turn. */
@@ -1,4 +1,4 @@
1
- import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
1
+ import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback, CustomMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Component } from "@oh-my-pi/pi-tui";
3
3
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
4
4
  import type { Theme } from "../modes/theme/theme";
@@ -18,10 +18,13 @@ export interface ResolveToolDetails {
18
18
  sourceResultDetails?: unknown;
19
19
  }
20
20
  /**
21
- * Queue a resolve-protocol handler on the tool-choice queue. Forces the next
22
- * LLM call to invoke the hidden `resolve` tool, wraps the caller's apply/reject
23
- * callbacks into an onInvoked closure that matches the resolve schema, and
24
- * steers a preview reminder so the model understands why.
21
+ * Register a non-forcing resolve-protocol handler for a staged preview. Wraps the
22
+ * caller's apply/reject into an onInvoked closure (matching the resolve schema) and
23
+ * stores it on the tool-choice queue's pending-invoker registry under a UNIQUE id.
24
+ * The `resolve` tool dispatches to it; the agent-loop's SoftToolRequirement
25
+ * lifecycle injects the preview reminder and escalates to a forced `resolve` only
26
+ * if the model declines — so a compliant turn pays ZERO tool_choice change (no
27
+ * prompt-cache messages-cache invalidation).
25
28
  *
26
29
  * This is the canonical entry point for any tool that wants preview/apply
27
30
  * semantics. No session-level abstraction is needed: callers pass their
@@ -33,6 +36,13 @@ export declare function queueResolveHandler(session: ToolSession, options: {
33
36
  apply(reason: string, extra?: Record<string, unknown>): Promise<AgentToolResult<unknown>>;
34
37
  reject?(reason: string, extra?: Record<string, unknown>): Promise<AgentToolResult<unknown> | undefined>;
35
38
  }): void;
39
+ /**
40
+ * The canonical preview reminder. The resolve mechanism owns the wording; the
41
+ * agent-loop delivers it via the session's `SoftToolRequirement.reminder` (injected
42
+ * once per pending-preview head) instead of a host-side steer, so it lands as a
43
+ * stable mid-history append and never churns the cached prefix.
44
+ */
45
+ export declare function buildResolveReminderMessage(sourceToolName: string): CustomMessage;
36
46
  /**
37
47
  * Shared invocation runner used by both queued (in-flight) handlers and
38
48
  * standing handlers (e.g. plan-mode approval). Discriminates on action,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-coding-agent",
4
- "version": "16.0.10",
4
+ "version": "16.1.0",
5
5
  "description": "Coding agent CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -48,17 +48,17 @@
48
48
  "@agentclientprotocol/sdk": "0.25.0",
49
49
  "@babel/parser": "^7.29.7",
50
50
  "@mozilla/readability": "^0.6.0",
51
- "@oh-my-pi/hashline": "16.0.10",
52
- "@oh-my-pi/omp-stats": "16.0.10",
53
- "@oh-my-pi/pi-agent-core": "16.0.10",
54
- "@oh-my-pi/pi-ai": "16.0.10",
55
- "@oh-my-pi/pi-catalog": "16.0.10",
56
- "@oh-my-pi/pi-mnemopi": "16.0.10",
57
- "@oh-my-pi/pi-natives": "16.0.10",
58
- "@oh-my-pi/pi-tui": "16.0.10",
59
- "@oh-my-pi/pi-utils": "16.0.10",
60
- "@oh-my-pi/pi-wire": "16.0.10",
61
- "@oh-my-pi/snapcompact": "16.0.10",
51
+ "@oh-my-pi/hashline": "16.1.0",
52
+ "@oh-my-pi/omp-stats": "16.1.0",
53
+ "@oh-my-pi/pi-agent-core": "16.1.0",
54
+ "@oh-my-pi/pi-ai": "16.1.0",
55
+ "@oh-my-pi/pi-catalog": "16.1.0",
56
+ "@oh-my-pi/pi-mnemopi": "16.1.0",
57
+ "@oh-my-pi/pi-natives": "16.1.0",
58
+ "@oh-my-pi/pi-tui": "16.1.0",
59
+ "@oh-my-pi/pi-utils": "16.1.0",
60
+ "@oh-my-pi/pi-wire": "16.1.0",
61
+ "@oh-my-pi/snapcompact": "16.1.0",
62
62
  "@opentelemetry/api": "^1.9.1",
63
63
  "@opentelemetry/context-async-hooks": "^2.7.1",
64
64
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -1,3 +1,4 @@
1
1
  export * from "./advise-tool";
2
2
  export * from "./runtime";
3
+ export * from "./transcript-recorder";
3
4
  export * from "./watchdog";
@@ -0,0 +1,136 @@
1
+ import * as path from "node:path";
2
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
3
+ import type { Message, UserMessage } from "@oh-my-pi/pi-ai";
4
+ import { logger } from "@oh-my-pi/pi-utils";
5
+ import { SessionManager } from "../session/session-manager";
6
+
7
+ /**
8
+ * Reserved transcript stem for advisor session files. Chosen so it cannot
9
+ * collide with a task subagent's `<id>.jsonl` (task ids are reserved against
10
+ * this exact stem in {@link AgentOutputManager}).
11
+ */
12
+ export const ADVISOR_TRANSCRIPT_STEM = "__advisor";
13
+ export const ADVISOR_TRANSCRIPT_FILENAME = `${ADVISOR_TRANSCRIPT_STEM}.jsonl`;
14
+
15
+ const JSONL_SUFFIX = ".jsonl";
16
+
17
+ /**
18
+ * Append-only persister for an advisor agent's transcript.
19
+ *
20
+ * The advisor is a passive reviewer with its own model usage, so — like a task
21
+ * subagent — its turns are written to a JSONL inside the owning session's
22
+ * artifacts dir (`<session>/__advisor.jsonl`, `<session>/<SubId>/__advisor.jsonl`
23
+ * for subagent advisors). That single file gives the advisor model proper usage
24
+ * attribution in `omp stats` (the stats parser scans the session dir
25
+ * recursively) and a read-only transcript in the Agent Hub, without making the
26
+ * advisor a registered, messageable peer.
27
+ *
28
+ * The target is derived from the *session file* (`getSessionFile()`), never
29
+ * `getArtifactsDir()` — subagents adopt the parent's artifact manager, so the
30
+ * artifacts dir points at the parent root and every subagent advisor would
31
+ * collide. The file path is resolved synchronously when a message finalizes and
32
+ * captured for the queued write, so a `/new`, resume, or session switch in
33
+ * flight can never misattribute an old advisor turn into the new session's file.
34
+ * On such a switch the previous writer is closed and the new file opened on the
35
+ * next recorded turn. The recorder never truncates: the advisor's in-memory
36
+ * context resets/compacts independently, but every billed turn is appended here.
37
+ */
38
+ export class AdvisorTranscriptRecorder {
39
+ #manager: SessionManager | undefined;
40
+ #file: string | undefined;
41
+ /** Serializes the async open/close against synchronous appends so records land in order. */
42
+ #queue: Promise<void>;
43
+
44
+ /**
45
+ * @param after Optional barrier the queue starts behind — used on the advisor
46
+ * on→off→on toggle so a fresh recorder's first `open` waits for the prior
47
+ * recorder's `close` and the two never hold the same `__advisor.jsonl` at once.
48
+ */
49
+ constructor(
50
+ private readonly resolveSessionFile: () => string | undefined,
51
+ private readonly resolveCwd: () => string,
52
+ after?: Promise<unknown>,
53
+ ) {
54
+ this.#queue = after
55
+ ? after.then(
56
+ () => {},
57
+ () => {},
58
+ )
59
+ : Promise.resolve();
60
+ }
61
+
62
+ /**
63
+ * Persist one finalized advisor message. Assistant turns carry the usage the
64
+ * stats parser reads; tool results round out the Hub transcript; user deltas
65
+ * (the advisor's "session update" prompts) are persisted but flagged
66
+ * `synthetic`/agent-attributed so they never inflate user-message metrics.
67
+ * Non-conversational message kinds are skipped.
68
+ */
69
+ record(message: AgentMessage): void {
70
+ let persisted: Message;
71
+ switch (message.role) {
72
+ case "assistant":
73
+ case "toolResult":
74
+ persisted = message;
75
+ break;
76
+ case "user":
77
+ // Clone so the live advisor message stays untouched; mark synthetic so
78
+ // stats' user-message metrics skip these agent-internal review prompts.
79
+ persisted = { ...(message as UserMessage), synthetic: true, attribution: "agent" };
80
+ break;
81
+ default:
82
+ return;
83
+ }
84
+ const sessionFile = this.resolveSessionFile();
85
+ if (!sessionFile?.endsWith(JSONL_SUFFIX)) return;
86
+ const file = path.join(sessionFile.slice(0, -JSONL_SUFFIX.length), ADVISOR_TRANSCRIPT_FILENAME);
87
+ const cwd = this.resolveCwd();
88
+ this.#enqueue(async () => {
89
+ if (file !== this.#file) {
90
+ await this.#closeManager();
91
+ this.#manager = await SessionManager.open(file, undefined, undefined, {
92
+ initialCwd: cwd,
93
+ suppressBreadcrumb: true,
94
+ });
95
+ this.#file = file;
96
+ }
97
+ this.#manager?.appendMessage(persisted);
98
+ });
99
+ }
100
+
101
+ /** Flush pending writes (best-effort). */
102
+ flush(): Promise<void> {
103
+ return this.#enqueueResult(async () => {
104
+ if (this.#manager) await this.#manager.flush();
105
+ });
106
+ }
107
+
108
+ /** Flush and close the writer, releasing the session file. */
109
+ close(): Promise<void> {
110
+ return this.#enqueueResult(() => this.#closeManager());
111
+ }
112
+
113
+ async #closeManager(): Promise<void> {
114
+ const manager = this.#manager;
115
+ this.#manager = undefined;
116
+ this.#file = undefined;
117
+ if (!manager) return;
118
+ try {
119
+ await manager.close();
120
+ } catch (err) {
121
+ logger.debug("advisor transcript close failed", { err: String(err) });
122
+ }
123
+ }
124
+
125
+ #enqueue(work: () => Promise<void>): void {
126
+ this.#queue = this.#queue.then(work, work).catch(err => {
127
+ logger.debug("advisor transcript record failed", { err: String(err) });
128
+ });
129
+ }
130
+
131
+ #enqueueResult(work: () => Promise<void>): Promise<void> {
132
+ const next = this.#queue.then(work, work);
133
+ this.#queue = next.catch(() => {});
134
+ return next;
135
+ }
136
+ }
@@ -4,6 +4,7 @@
4
4
  * Handles `omp stats` subcommand for viewing AI usage statistics.
5
5
  */
6
6
 
7
+ import { truncateToWidth } from "@oh-my-pi/pi-tui/utils";
7
8
  import { APP_NAME, formatDuration, formatNumber, formatPercent } from "@oh-my-pi/pi-utils";
8
9
  import chalk from "chalk";
9
10
  import { openPath } from "../utils/open";
@@ -32,7 +33,7 @@ function createSyncProgressReporter(): {
32
33
  const counter = chalk.cyan(`[${event.current}/${event.total}]`);
33
34
  const line = `${counter} ${pct}% ${label}`;
34
35
  const columns = stream.columns ?? 120;
35
- const trimmed = truncateToColumns(line, columns - 1);
36
+ const trimmed = truncateToWidth(line, columns - 1);
36
37
  stream.write(`\r${trimmed.padEnd(lastWidth)}`);
37
38
  lastWidth = trimmed.length;
38
39
  },
@@ -50,16 +51,6 @@ function shortenSessionFile(p: string): string {
50
51
  return idx >= 0 ? p.slice(idx + marker.length) : p;
51
52
  }
52
53
 
53
- function truncateToColumns(s: string, max: number): string {
54
- if (max <= 0) return "";
55
- const width = Bun.stringWidth(s, { countAnsiEscapeCodes: false });
56
- if (width <= max) return s;
57
- // Cheap right-trim with an ellipsis - we don't need ANSI-aware slicing
58
- // because the colored prefix is short and the truncated tail is the
59
- // dim filename, where dropping bytes is fine.
60
- return `${s.slice(0, Math.max(0, max - 1))}\u2026`;
61
- }
62
-
63
54
  // =============================================================================
64
55
  // Types
65
56
  // =============================================================================
@@ -17,7 +17,7 @@ import { logger } from "@oh-my-pi/pi-utils";
17
17
  import type { BusChannel, AgentEvent as WireAgentEvent, SessionEntry as WireSessionEntry } from "@oh-my-pi/pi-wire";
18
18
  import type { InteractiveModeContext } from "../modes/types";
19
19
  import { AgentLifecycleManager } from "../registry/agent-lifecycle";
20
- import { AgentRegistry } from "../registry/agent-registry";
20
+ import { type AgentRef, AgentRegistry } from "../registry/agent-registry";
21
21
  import type { AgentSessionEvent } from "../session/agent-session";
22
22
  import { stripImagesFromMessage, USER_INTERRUPT_LABEL } from "../session/messages";
23
23
  import type { SessionEntry as StoredSessionEntry } from "../session/session-entries";
@@ -445,18 +445,24 @@ export class CollabHost {
445
445
  }
446
446
 
447
447
  #snapshotAgents(): AgentSnapshot[] {
448
- return AgentRegistry.global()
449
- .list()
450
- .map(ref => ({
451
- id: ref.id,
452
- displayName: ref.displayName,
453
- kind: ref.kind,
454
- parentId: ref.parentId,
455
- status: ref.status,
456
- hasSessionFile: !!ref.sessionFile,
457
- createdAt: ref.createdAt,
458
- lastActivity: ref.lastActivity,
459
- }));
448
+ return (
449
+ AgentRegistry.global()
450
+ .list()
451
+ // Advisor transcripts are local observability only; never mirror them to
452
+ // guests (the wire AgentSnapshot kind has no `advisor`, and guests must not
453
+ // be able to chat/kill/revive them).
454
+ .filter((ref): ref is AgentRef & { kind: "main" | "sub" } => ref.kind !== "advisor")
455
+ .map(ref => ({
456
+ id: ref.id,
457
+ displayName: ref.displayName,
458
+ kind: ref.kind,
459
+ parentId: ref.parentId,
460
+ status: ref.status,
461
+ hasSessionFile: !!ref.sessionFile,
462
+ createdAt: ref.createdAt,
463
+ lastActivity: ref.lastActivity,
464
+ }))
465
+ );
460
466
  }
461
467
 
462
468
  #scheduleAgentsBroadcast(): void {
@@ -472,6 +478,12 @@ export class CollabHost {
472
478
  this.#rejectReadOnly("agent control", fromPeer);
473
479
  return;
474
480
  }
481
+ // Advisor refs are excluded from snapshots, but reject control by id defensively:
482
+ // a stale/malicious client must never chat/kill/revive a read-only advisor transcript.
483
+ if (AgentRegistry.global().get(agentId)?.kind === "advisor") {
484
+ this.#socket?.send({ t: "error", message: `agent ${agentId}: advisor transcripts are read-only` }, fromPeer);
485
+ return;
486
+ }
475
487
  const fail = (err: unknown) => {
476
488
  logger.warn("collab agent-cmd failed", { cmd, agentId, error: String(err) });
477
489
  this.#socket?.send({ t: "error", message: `agent ${agentId}: ${String(err)}` }, fromPeer);
@@ -1,7 +1,8 @@
1
- import { INTENT_FIELD, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Api, Model } from "@oh-my-pi/pi-ai";
3
3
  import { Markdown } from "@oh-my-pi/pi-tui";
4
4
  import { prompt } from "@oh-my-pi/pi-utils";
5
+ import { INTENT_FIELD } from "@oh-my-pi/pi-wire";
5
6
  import chalk from "chalk";
6
7
  import typesDescriptionPrompt from "../../commit/prompts/types-description.md" with { type: "text" };
7
8
  import type { ModelRegistry } from "../../config/model-registry";
@@ -87,7 +87,7 @@ function truncateDiffContent(diff: string): { content: string; truncated: boolea
87
87
  const truncatedCount = lines.length - KEEP_HEAD_LINES - KEEP_TAIL_LINES;
88
88
 
89
89
  return {
90
- content: [...head, `\n... (truncated ${truncatedCount} lines) ...\n`, ...tail].join("\n"),
90
+ content: [...head, `\n[…${truncatedCount}ln elided…]\n`, ...tail].join("\n"),
91
91
  truncated: true,
92
92
  };
93
93
  }
@@ -117,7 +117,7 @@ function processDiffs(files: string[], diffs: Map<string, string>): { result: st
117
117
  }
118
118
  content = truncated;
119
119
  if (content.length > remaining) {
120
- content = `${content.slice(0, remaining)}\n... (diff truncated due to size) ...`;
120
+ content = `${content.slice(0, remaining)}\n[…${content.length - remaining}ch elided…]`;
121
121
  if (!truncatedFiles.includes(file)) {
122
122
  truncatedFiles.push(file);
123
123
  }
@@ -138,7 +138,7 @@ export async function applyChangelogProposals({
138
138
 
139
139
  function truncateDiff(diff: string, maxChars: number): string {
140
140
  if (diff.length <= maxChars) return diff;
141
- return `${diff.slice(0, maxChars)}\n... (truncated)`;
141
+ return `${diff.slice(0, maxChars)}\n[…${diff.length - maxChars}ch elided…]`;
142
142
  }
143
143
 
144
144
  function formatExistingEntries(entries: Record<string, string[]>): string {
@@ -126,7 +126,7 @@ function generateContextHeader(files: FileDiff[], currentFile: string): string {
126
126
  }
127
127
 
128
128
  if (toShow.length < sorted.length) {
129
- lines.push(`... and ${sorted.length - toShow.length} more files`);
129
+ lines.push(`[…${sorted.length - toShow.length} files elided…]`);
130
130
  }
131
131
 
132
132
  return lines.join("\n");
@@ -5,5 +5,5 @@ export function estimateTokens(text: string): number {
5
5
  export function truncateToTokenLimit(text: string, maxTokens: number): string {
6
6
  const maxChars = maxTokens * 4;
7
7
  if (text.length <= maxChars) return text;
8
- return `${text.slice(0, maxChars)}\n... (truncated)`;
8
+ return `${text.slice(0, maxChars)}\n[…${text.length - maxChars}ch elided…]`;
9
9
  }
@@ -809,11 +809,6 @@ export const SETTINGS_SCHEMA = {
809
809
  description: "Remove the 1-character horizontal padding from the left and right of the terminal output",
810
810
  },
811
811
  },
812
- // Display rendering
813
- "display.tabWidth": {
814
- type: "number",
815
- default: 3,
816
- },
817
812
 
818
813
  "display.shimmer": {
819
814
  type: "enum",
@@ -854,6 +849,17 @@ export const SETTINGS_SCHEMA = {
854
849
  },
855
850
  },
856
851
 
852
+ "display.cacheMissMarker": {
853
+ type: "boolean",
854
+ default: true,
855
+ ui: {
856
+ tab: "appearance",
857
+ group: "Display",
858
+ label: "Cache Miss Marker",
859
+ description: "Show a divider above an assistant turn whose request lost (missed) the prompt cache",
860
+ },
861
+ },
862
+
857
863
  showHardwareCursor: {
858
864
  type: "boolean",
859
865
  default: true, // will be computed based on platform if undefined
@@ -919,14 +925,15 @@ export const SETTINGS_SCHEMA = {
919
925
  },
920
926
  },
921
927
 
922
- repeatToolDescriptions: {
928
+ inlineToolDescriptors: {
923
929
  type: "boolean",
924
- default: false,
930
+ default: true,
925
931
  ui: {
926
932
  tab: "model",
927
933
  group: "Prompt",
928
- label: "Repeat Tool Descriptions",
929
- description: "Render full tool descriptions in the system prompt instead of a tool name list",
934
+ label: "Inline Tool Descriptors",
935
+ description:
936
+ "Render full tool descriptors in the system prompt and strip top-level/nested descriptions from provider tool schemas so descriptor text is sent once",
930
937
  },
931
938
  },
932
939
 
@@ -22,7 +22,6 @@ import {
22
22
  isEnoent,
23
23
  logger,
24
24
  procmgr,
25
- setDefaultTabWidth,
26
25
  } from "@oh-my-pi/pi-utils";
27
26
  import { JSONC, YAML } from "bun";
28
27
  import { type Settings as SettingsCapabilityItem, settingsCapability } from "../capability/settings";
@@ -1103,11 +1102,6 @@ const SETTING_HOOKS: Partial<Record<SettingPath, SettingHook<any>>> = {
1103
1102
  });
1104
1103
  }
1105
1104
  },
1106
- "display.tabWidth": value => {
1107
- if (typeof value === "number") {
1108
- setDefaultTabWidth(value);
1109
- }
1110
- },
1111
1105
  "provider.appendOnlyContext": value => {
1112
1106
  if (typeof value === "string") {
1113
1107
  appendOnlyModeSignal.fire(value);
@@ -866,21 +866,21 @@ export class DebugLogViewerComponent implements Component {
866
866
  }
867
867
 
868
868
  #frameTop(innerWidth: number): string {
869
- return `${theme.boxSharp.topLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.topRight}`;
869
+ return `${theme.boxRound.topLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.topRight}`;
870
870
  }
871
871
 
872
872
  #frameSeparator(innerWidth: number): string {
873
- return `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.teeLeft}`;
873
+ return `${theme.boxRound.teeRight}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.teeLeft}`;
874
874
  }
875
875
 
876
876
  #frameBottom(innerWidth: number): string {
877
- return `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.bottomRight}`;
877
+ return `${theme.boxRound.bottomLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.bottomRight}`;
878
878
  }
879
879
 
880
880
  #frameLine(content: string, innerWidth: number): string {
881
881
  const truncated = truncateToWidth(content, innerWidth);
882
882
  const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
883
- return `${theme.boxSharp.vertical}${truncated}${padding(remaining)}${theme.boxSharp.vertical}`;
883
+ return `${theme.boxRound.vertical}${truncated}${padding(remaining)}${theme.boxRound.vertical}`;
884
884
  }
885
885
 
886
886
  #copySelected() {
@@ -273,20 +273,20 @@ export class RawSseViewerComponent implements Component {
273
273
  }
274
274
 
275
275
  #frameTop(innerWidth: number): string {
276
- return `${theme.boxSharp.topLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.topRight}`;
276
+ return `${theme.boxRound.topLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.topRight}`;
277
277
  }
278
278
 
279
279
  #frameSeparator(innerWidth: number): string {
280
- return `${theme.boxSharp.teeRight}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.teeLeft}`;
280
+ return `${theme.boxRound.teeRight}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.teeLeft}`;
281
281
  }
282
282
 
283
283
  #frameBottom(innerWidth: number): string {
284
- return `${theme.boxSharp.bottomLeft}${theme.boxSharp.horizontal.repeat(innerWidth)}${theme.boxSharp.bottomRight}`;
284
+ return `${theme.boxRound.bottomLeft}${theme.boxRound.horizontal.repeat(innerWidth)}${theme.boxRound.bottomRight}`;
285
285
  }
286
286
 
287
287
  #frameLine(content: string, innerWidth: number): string {
288
288
  const truncated = truncateToWidth(content, innerWidth);
289
289
  const remaining = Math.max(0, innerWidth - visibleWidth(truncated));
290
- return `${theme.boxSharp.vertical}${truncated}${padding(remaining)}${theme.boxSharp.vertical}`;
290
+ return `${theme.boxRound.vertical}${truncated}${padding(remaining)}${theme.boxRound.vertical}`;
291
291
  }
292
292
  }