@oh-my-pi/pi-coding-agent 15.11.7 → 15.12.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 (107) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/dist/cli.js +8106 -7708
  3. package/dist/types/cli/args.d.ts +2 -0
  4. package/dist/types/collab/crypto.d.ts +7 -0
  5. package/dist/types/collab/guest.d.ts +23 -0
  6. package/dist/types/collab/host.d.ts +29 -0
  7. package/dist/types/collab/protocol.d.ts +113 -0
  8. package/dist/types/collab/relay-client.d.ts +22 -0
  9. package/dist/types/commands/join.d.ts +12 -0
  10. package/dist/types/config/settings-schema.d.ts +60 -5
  11. package/dist/types/export/custom-share.d.ts +1 -2
  12. package/dist/types/export/html/index.d.ts +39 -1
  13. package/dist/types/export/share.d.ts +43 -0
  14. package/dist/types/extensibility/slash-commands.d.ts +1 -11
  15. package/dist/types/main.d.ts +2 -0
  16. package/dist/types/modes/components/agent-hub.d.ts +32 -1
  17. package/dist/types/modes/components/collab-prompt-message.d.ts +10 -0
  18. package/dist/types/modes/components/hook-selector.d.ts +4 -6
  19. package/dist/types/modes/components/segment-track.d.ts +11 -6
  20. package/dist/types/modes/components/status-line/component.d.ts +10 -2
  21. package/dist/types/modes/components/status-line/types.d.ts +11 -0
  22. package/dist/types/modes/controllers/event-controller.d.ts +7 -0
  23. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  24. package/dist/types/modes/controllers/session-focus-controller.d.ts +31 -0
  25. package/dist/types/modes/interactive-mode.d.ts +16 -0
  26. package/dist/types/modes/session-observer-registry.d.ts +7 -0
  27. package/dist/types/modes/theme/theme.d.ts +2 -1
  28. package/dist/types/modes/types.d.ts +20 -0
  29. package/dist/types/session/agent-session.d.ts +13 -0
  30. package/dist/types/session/codex-auto-reset.d.ts +8 -4
  31. package/dist/types/session/session-manager.d.ts +21 -0
  32. package/dist/types/session/snapcompact-inline.d.ts +6 -3
  33. package/dist/types/slash-commands/builtin-registry.d.ts +9 -0
  34. package/dist/types/task/executor.d.ts +7 -0
  35. package/dist/types/task/types.d.ts +9 -0
  36. package/package.json +14 -13
  37. package/scripts/bench-guard.ts +71 -0
  38. package/scripts/build-binary.ts +4 -0
  39. package/scripts/bundle-dist.ts +4 -0
  40. package/scripts/generate-share-viewer.ts +34 -0
  41. package/src/cli/args.ts +2 -0
  42. package/src/cli-commands.ts +1 -0
  43. package/src/collab/crypto.ts +63 -0
  44. package/src/collab/guest.ts +450 -0
  45. package/src/collab/host.ts +556 -0
  46. package/src/collab/protocol.ts +232 -0
  47. package/src/collab/relay-client.ts +216 -0
  48. package/src/commands/join.ts +39 -0
  49. package/src/config/model-registry.ts +22 -14
  50. package/src/config/settings-schema.ts +67 -5
  51. package/src/config/settings.ts +12 -0
  52. package/src/export/custom-share.ts +1 -1
  53. package/src/export/html/index.ts +122 -17
  54. package/src/export/html/share-loader.js +102 -0
  55. package/src/export/html/template.css +745 -459
  56. package/src/export/html/template.html +6 -3
  57. package/src/export/html/template.js +240 -915
  58. package/src/export/html/tool-views.generated.js +38 -0
  59. package/src/export/share.ts +268 -0
  60. package/src/extensibility/slash-commands.ts +1 -97
  61. package/src/internal-urls/docs-index.generated.ts +74 -73
  62. package/src/main.ts +33 -11
  63. package/src/modes/components/agent-hub.ts +659 -431
  64. package/src/modes/components/assistant-message.ts +126 -6
  65. package/src/modes/components/collab-prompt-message.ts +30 -0
  66. package/src/modes/components/hook-selector.ts +4 -5
  67. package/src/modes/components/segment-track.ts +44 -7
  68. package/src/modes/components/status-line/component.ts +59 -6
  69. package/src/modes/components/status-line/presets.ts +1 -1
  70. package/src/modes/components/status-line/segments.ts +18 -1
  71. package/src/modes/components/status-line/types.ts +12 -0
  72. package/src/modes/components/tips.txt +4 -1
  73. package/src/modes/controllers/command-controller.ts +55 -96
  74. package/src/modes/controllers/event-controller.ts +45 -16
  75. package/src/modes/controllers/input-controller.ts +175 -9
  76. package/src/modes/controllers/selector-controller.ts +13 -15
  77. package/src/modes/controllers/session-focus-controller.ts +112 -0
  78. package/src/modes/controllers/streaming-reveal.ts +7 -0
  79. package/src/modes/interactive-mode.ts +56 -6
  80. package/src/modes/session-observer-registry.ts +11 -0
  81. package/src/modes/theme/theme.ts +6 -0
  82. package/src/modes/types.ts +20 -0
  83. package/src/modes/utils/ui-helpers.ts +23 -13
  84. package/src/prompts/tools/job.md +1 -1
  85. package/src/sdk.ts +239 -36
  86. package/src/session/agent-session.ts +82 -7
  87. package/src/session/codex-auto-reset.ts +23 -11
  88. package/src/session/session-manager.ts +44 -0
  89. package/src/session/snapcompact-inline.ts +9 -3
  90. package/src/slash-commands/builtin-registry.ts +261 -24
  91. package/src/task/executor.ts +14 -0
  92. package/src/task/index.ts +5 -1
  93. package/src/task/render.ts +76 -5
  94. package/src/task/types.ts +9 -0
  95. package/src/tiny/worker.ts +17 -95
  96. package/src/tools/job.ts +6 -9
  97. package/src/tools/read.ts +38 -5
  98. package/src/tools/write.ts +13 -42
  99. package/dist/tokenizers.linux-x64-gnu-xcjh3jwk.node +0 -0
  100. package/dist/types/export/html/template.generated.d.ts +0 -1
  101. package/dist/types/export/html/template.macro.d.ts +0 -5
  102. package/dist/types/tiny/compiled-runtime.d.ts +0 -35
  103. package/scripts/generate-template.ts +0 -33
  104. package/src/bun-imports.d.ts +0 -28
  105. package/src/export/html/template.generated.ts +0 -2
  106. package/src/export/html/template.macro.ts +0 -25
  107. package/src/tiny/compiled-runtime.ts +0 -179
@@ -23,6 +23,13 @@ export declare class EventController {
23
23
  */
24
24
  notifyInterrupting(): void;
25
25
  subscribeToAgent(): void;
26
+ /**
27
+ * Clear every transcript-anchored/turn-scoped piece of state. Used by the
28
+ * session focus proxy when re-pointing the transcript at another session:
29
+ * components, timers, and stream-reveal state all reference the previous
30
+ * session's transcript and must not bleed into the new one.
31
+ */
32
+ resetTranscriptAnchors(): void;
26
33
  handleEvent(event: AgentSessionEvent): Promise<void>;
27
34
  sendCompletionNotification(): void;
28
35
  }
@@ -1,4 +1,4 @@
1
- import type { AutocompleteProvider, SlashCommand } from "@oh-my-pi/pi-tui";
1
+ import { type AutocompleteProvider, type SlashCommand } from "@oh-my-pi/pi-tui";
2
2
  import type { InteractiveModeContext } from "../../modes/types";
3
3
  import { readImageFromClipboard, readTextFromClipboard } from "../../utils/clipboard";
4
4
  export declare class InputController {
@@ -0,0 +1,31 @@
1
+ /**
2
+ * SessionFocusController - Weak retargeting primitive between the rendering/
3
+ * input layer and the AgentSession it displays.
4
+ *
5
+ * Focusing re-points the transcript, streaming event subscription, status
6
+ * line, and editor prompt/interrupt at a subagent's live AgentSession (from
7
+ * AgentRegistry) without touching the main session underneath; unfocusing
8
+ * re-attaches the main session and rebuilds the transcript from its
9
+ * authoritative state.
10
+ */
11
+ import { AgentLifecycleManager } from "../../registry/agent-lifecycle";
12
+ import { AgentRegistry } from "../../registry/agent-registry";
13
+ import type { AgentSession } from "../../session/agent-session";
14
+ import type { InteractiveModeContext } from "../types";
15
+ export declare class SessionFocusController {
16
+ #private;
17
+ private ctx;
18
+ private registry;
19
+ private lifecycle;
20
+ constructor(ctx: InteractiveModeContext, registry?: AgentRegistry, lifecycle?: () => AgentLifecycleManager);
21
+ get focusedAgentId(): string | undefined;
22
+ /** Focused live session, undefined when unfocused. */
23
+ get target(): AgentSession | undefined;
24
+ /** Focus the main view on an agent's live session. Throws an Error with a user-displayable message. */
25
+ focusAgent(id: string): Promise<void>;
26
+ /** Focus the focused agent's parent agent, falling back to the main session. No-op when unfocused. */
27
+ focusParent(): Promise<void>;
28
+ /** Return to the main session. No-op when unfocused. */
29
+ unfocus(): Promise<void>;
30
+ dispose(): void;
31
+ }
@@ -3,6 +3,8 @@ import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
4
4
  import type { Component, EditorTheme } from "@oh-my-pi/pi-tui";
5
5
  import { Container, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
6
+ import type { CollabGuestLink } from "../collab/guest";
7
+ import type { CollabHost } from "../collab/host";
6
8
  import { KeybindingsManager } from "../config/keybindings";
7
9
  import { Settings } from "../config/settings";
8
10
  import type { ExtensionUIContext, ExtensionUIDialogOptions, ExtensionUISelectItem, ExtensionWidgetContent, ExtensionWidgetOptions } from "../extensibility/extensions";
@@ -25,6 +27,7 @@ import type { HookSelectorComponent, HookSelectorSlider } from "./components/hoo
25
27
  import { StatusLineComponent } from "./components/status-line";
26
28
  import type { ToolExecutionHandle } from "./components/tool-execution";
27
29
  import { TranscriptContainer } from "./components/transcript-container";
30
+ import { EventController } from "./controllers/event-controller";
28
31
  import { type LoopLimitRuntime } from "./loop-limit";
29
32
  import { OAuthManualInputManager } from "./oauth-manual-input";
30
33
  import type { ObservableSession } from "./session-observer-registry";
@@ -47,6 +50,9 @@ export interface InteractiveModeOptions {
47
50
  * Build the anchored subagent HUD block: a bold accent "Subagents" header plus
48
51
  * one hooked row per running agent in the same `Id: description` shape the
49
52
  * inline task rows use (muted task preview when no description was given).
53
+ * Only detached background spawns are listed: a sync task call blocks the
54
+ * parent turn and its inline tool block already renders progress live, and
55
+ * eval `agent()` spawns are rendered by their own eval cell tree.
50
56
  * Returns an empty array when nothing is running so the container can clear.
51
57
  */
52
58
  export declare function renderSubagentHudLines(sessions: ObservableSession[], columns: number): string[];
@@ -119,8 +125,18 @@ export declare class InteractiveMode implements InteractiveModeContext {
119
125
  fileSlashCommands: Set<string>;
120
126
  skillCommands: Map<string, string>;
121
127
  oauthManualInput: OAuthManualInputManager;
128
+ collabHost?: CollabHost;
129
+ collabGuest?: CollabGuestLink;
122
130
  readonly lspServers: LspStartupServerInfo[] | undefined;
123
131
  mcpManager?: MCPManager;
132
+ get eventController(): EventController;
133
+ get eventBus(): EventBus | undefined;
134
+ get viewSession(): AgentSession;
135
+ get focusedAgentId(): string | undefined;
136
+ focusAgentSession(id: string): Promise<void>;
137
+ focusParentSession(): Promise<void>;
138
+ unfocusSession(): Promise<void>;
139
+ clearTransientSessionUi(): void;
124
140
  constructor(session: AgentSession, version: string, changelogMarkdown?: string | undefined, setToolUIContext?: (uiContext: ExtensionUIContext, hasUI: boolean) => void, lspServers?: LspStartupServerInfo[] | undefined, mcpManager?: MCPManager, eventBus?: EventBus, titleSystemPrompt?: string);
125
141
  playWelcomeIntro(): void;
126
142
  init(options?: InteractiveModeInitOptions): Promise<void>;
@@ -9,6 +9,13 @@ export interface ObservableSession {
9
9
  status: "active" | "completed" | "failed" | "aborted";
10
10
  sessionFile?: string;
11
11
  parentToolCallId?: string;
12
+ /**
13
+ * Spawn runs as a detached background job (parent turn not blocked on it).
14
+ * The anchored subagent HUD only lists detached spawns: sync task spawns
15
+ * and eval `agent()` spawns are already rendered live by their own inline
16
+ * tool block / eval cell.
17
+ */
18
+ detached?: boolean;
12
19
  index?: number;
13
20
  lastUpdate: number;
14
21
  /** Latest progress snapshot from the subagent executor */
@@ -6,7 +6,7 @@ export type SymbolPreset = "unicode" | "nerd" | "ascii";
6
6
  /**
7
7
  * All available symbol keys organized by category.
8
8
  */
9
- export type SymbolKey = "status.success" | "status.error" | "status.warning" | "status.info" | "status.pending" | "status.disabled" | "status.enabled" | "status.running" | "status.shadowed" | "status.aborted" | "status.done" | "nav.cursor" | "nav.selected" | "nav.expand" | "nav.collapse" | "nav.back" | "tree.branch" | "tree.last" | "tree.vertical" | "tree.horizontal" | "tree.hook" | "boxRound.topLeft" | "boxRound.topRight" | "boxRound.bottomLeft" | "boxRound.bottomRight" | "boxRound.horizontal" | "boxRound.vertical" | "boxSharp.topLeft" | "boxSharp.topRight" | "boxSharp.bottomLeft" | "boxSharp.bottomRight" | "boxSharp.horizontal" | "boxSharp.vertical" | "boxSharp.cross" | "boxSharp.teeDown" | "boxSharp.teeUp" | "boxSharp.teeRight" | "boxSharp.teeLeft" | "sep.powerline" | "sep.powerlineThin" | "sep.powerlineLeft" | "sep.powerlineRight" | "sep.powerlineThinLeft" | "sep.powerlineThinRight" | "sep.block" | "sep.space" | "sep.asciiLeft" | "sep.asciiRight" | "sep.dot" | "sep.slash" | "sep.pipe" | "icon.model" | "icon.plan" | "icon.goal" | "icon.pause" | "icon.loop" | "icon.folder" | "icon.search" | "icon.scratchFolder" | "icon.file" | "icon.git" | "icon.branch" | "icon.pr" | "icon.tokens" | "icon.context" | "icon.cost" | "icon.time" | "icon.pi" | "icon.agents" | "icon.job" | "icon.cache" | "icon.input" | "icon.output" | "icon.host" | "icon.session" | "icon.package" | "icon.warning" | "icon.rewind" | "icon.auto" | "icon.fast" | "icon.extensionSkill" | "icon.extensionTool" | "icon.extensionSlashCommand" | "icon.extensionMcp" | "icon.extensionRule" | "icon.extensionHook" | "icon.extensionPrompt" | "icon.extensionContextFile" | "icon.extensionInstruction" | "icon.mic" | "icon.camera" | "thinking.minimal" | "thinking.low" | "thinking.medium" | "thinking.high" | "thinking.xhigh" | "thinking.autoPending" | "checkbox.checked" | "checkbox.unchecked" | "radio.selected" | "radio.unselected" | "format.bullet" | "format.dash" | "format.bracketLeft" | "format.bracketRight" | "md.quoteBorder" | "md.hrChar" | "md.bullet" | "md.colorSwatch" | "lang.default" | "lang.typescript" | "lang.javascript" | "lang.python" | "lang.rust" | "lang.go" | "lang.java" | "lang.c" | "lang.cpp" | "lang.csharp" | "lang.ruby" | "lang.php" | "lang.swift" | "lang.kotlin" | "lang.shell" | "lang.html" | "lang.css" | "lang.json" | "lang.yaml" | "lang.markdown" | "lang.sql" | "lang.docker" | "lang.lua" | "lang.text" | "lang.env" | "lang.toml" | "lang.xml" | "lang.ini" | "lang.conf" | "lang.log" | "lang.csv" | "lang.tsv" | "lang.image" | "lang.pdf" | "lang.archive" | "lang.binary" | "tab.appearance" | "tab.model" | "tab.interaction" | "tab.context" | "tab.files" | "tab.shell" | "tab.tools" | "tab.memory" | "tab.tasks" | "tab.providers" | "tool.write" | "tool.edit" | "tool.bash" | "tool.ssh" | "tool.lsp" | "tool.gh" | "tool.webSearch" | "tool.exa" | "tool.browser" | "tool.eval" | "tool.debug" | "tool.mcp" | "tool.job" | "tool.task" | "tool.todo" | "tool.memory" | "tool.ask" | "tool.resolve" | "tool.review" | "tool.inspectImage" | "tool.goal" | "tool.irc";
9
+ export type SymbolKey = "status.success" | "status.error" | "status.warning" | "status.info" | "status.pending" | "status.disabled" | "status.enabled" | "status.running" | "status.shadowed" | "status.aborted" | "status.done" | "nav.cursor" | "nav.selected" | "nav.expand" | "nav.collapse" | "nav.back" | "tree.branch" | "tree.last" | "tree.vertical" | "tree.horizontal" | "tree.hook" | "boxRound.topLeft" | "boxRound.topRight" | "boxRound.bottomLeft" | "boxRound.bottomRight" | "boxRound.horizontal" | "boxRound.vertical" | "boxSharp.topLeft" | "boxSharp.topRight" | "boxSharp.bottomLeft" | "boxSharp.bottomRight" | "boxSharp.horizontal" | "boxSharp.vertical" | "boxSharp.cross" | "boxSharp.teeDown" | "boxSharp.teeUp" | "boxSharp.teeRight" | "boxSharp.teeLeft" | "sep.powerline" | "sep.powerlineThin" | "sep.powerlineLeft" | "sep.powerlineRight" | "sep.powerlineThinLeft" | "sep.powerlineThinRight" | "sep.block" | "sep.space" | "sep.asciiLeft" | "sep.asciiRight" | "sep.dot" | "sep.slash" | "sep.pipe" | "icon.model" | "icon.plan" | "icon.goal" | "icon.pause" | "icon.loop" | "icon.folder" | "icon.search" | "icon.scratchFolder" | "icon.file" | "icon.git" | "icon.branch" | "icon.pr" | "icon.tokens" | "icon.context" | "icon.cost" | "icon.time" | "icon.pi" | "icon.ghost" | "icon.agents" | "icon.job" | "icon.cache" | "icon.input" | "icon.output" | "icon.host" | "icon.session" | "icon.package" | "icon.warning" | "icon.rewind" | "icon.auto" | "icon.fast" | "icon.extensionSkill" | "icon.extensionTool" | "icon.extensionSlashCommand" | "icon.extensionMcp" | "icon.extensionRule" | "icon.extensionHook" | "icon.extensionPrompt" | "icon.extensionContextFile" | "icon.extensionInstruction" | "icon.mic" | "icon.camera" | "thinking.minimal" | "thinking.low" | "thinking.medium" | "thinking.high" | "thinking.xhigh" | "thinking.autoPending" | "checkbox.checked" | "checkbox.unchecked" | "radio.selected" | "radio.unselected" | "format.bullet" | "format.dash" | "format.bracketLeft" | "format.bracketRight" | "md.quoteBorder" | "md.hrChar" | "md.bullet" | "md.colorSwatch" | "lang.default" | "lang.typescript" | "lang.javascript" | "lang.python" | "lang.rust" | "lang.go" | "lang.java" | "lang.c" | "lang.cpp" | "lang.csharp" | "lang.ruby" | "lang.php" | "lang.swift" | "lang.kotlin" | "lang.shell" | "lang.html" | "lang.css" | "lang.json" | "lang.yaml" | "lang.markdown" | "lang.sql" | "lang.docker" | "lang.lua" | "lang.text" | "lang.env" | "lang.toml" | "lang.xml" | "lang.ini" | "lang.conf" | "lang.log" | "lang.csv" | "lang.tsv" | "lang.image" | "lang.pdf" | "lang.archive" | "lang.binary" | "tab.appearance" | "tab.model" | "tab.interaction" | "tab.context" | "tab.files" | "tab.shell" | "tab.tools" | "tab.memory" | "tab.tasks" | "tab.providers" | "tool.write" | "tool.edit" | "tool.bash" | "tool.ssh" | "tool.lsp" | "tool.gh" | "tool.webSearch" | "tool.exa" | "tool.browser" | "tool.eval" | "tool.debug" | "tool.mcp" | "tool.job" | "tool.task" | "tool.todo" | "tool.memory" | "tool.ask" | "tool.resolve" | "tool.review" | "tool.inspectImage" | "tool.goal" | "tool.irc";
10
10
  export type SpinnerType = "status" | "activity";
11
11
  export type ThemeColor = "accent" | "border" | "borderAccent" | "borderMuted" | "success" | "error" | "warning" | "muted" | "dim" | "text" | "thinkingText" | "userMessageText" | "customMessageText" | "customMessageLabel" | "toolTitle" | "toolOutput" | "mdHeading" | "mdLink" | "mdLinkUrl" | "mdCode" | "mdCodeBlock" | "mdCodeBlockBorder" | "mdQuote" | "mdQuoteBorder" | "mdHr" | "mdListBullet" | "toolDiffAdded" | "toolDiffRemoved" | "toolDiffContext" | "syntaxComment" | "syntaxKeyword" | "syntaxFunction" | "syntaxVariable" | "syntaxString" | "syntaxNumber" | "syntaxType" | "syntaxOperator" | "syntaxPunctuation" | "thinkingOff" | "thinkingMinimal" | "thinkingLow" | "thinkingMedium" | "thinkingHigh" | "thinkingXhigh" | "bashMode" | "pythonMode" | "statusLineSep" | "statusLineModel" | "statusLinePath" | "statusLineGitClean" | "statusLineGitDirty" | "statusLineContext" | "statusLineSpend" | "statusLineStaged" | "statusLineDirty" | "statusLineUntracked" | "statusLineOutput" | "statusLineCost" | "statusLineSubagents";
12
12
  /** Check if a string is a valid ThemeColor value */
@@ -169,6 +169,7 @@ export declare class Theme {
169
169
  cost: string;
170
170
  time: string;
171
171
  pi: string;
172
+ ghost: string;
172
173
  agents: string;
173
174
  job: string;
174
175
  cache: string;
@@ -2,6 +2,8 @@ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
2
2
  import type { CompactionOutcome } from "@oh-my-pi/pi-agent-core/compaction";
3
3
  import type { AssistantMessage, ImageContent, Message, UsageReport } from "@oh-my-pi/pi-ai";
4
4
  import type { Component, Container, EditorTheme, Loader, Spacer, Text, TUI } from "@oh-my-pi/pi-tui";
5
+ import type { CollabGuestLink } from "../collab/guest";
6
+ import type { CollabHost } from "../collab/host";
5
7
  import type { KeybindingsManager } from "../config/keybindings";
6
8
  import type { Settings } from "../config/settings";
7
9
  import type { ExtensionUIContext, ExtensionUIDialogOptions, ExtensionUISelectItem, ExtensionWidgetContent, ExtensionWidgetOptions } from "../extensibility/extensions";
@@ -13,6 +15,7 @@ import type { HistoryStorage } from "../session/history-storage";
13
15
  import type { SessionContext, SessionManager } from "../session/session-manager";
14
16
  import type { ShakeMode } from "../session/shake-types";
15
17
  import type { LspStartupServerInfo } from "../tools";
18
+ import type { EventBus } from "../utils/event-bus";
16
19
  import type { AssistantMessageComponent } from "./components/assistant-message";
17
20
  import type { BashExecutionComponent } from "./components/bash-execution";
18
21
  import type { CustomEditor } from "./components/custom-editor";
@@ -23,6 +26,7 @@ import type { HookSelectorComponent, HookSelectorOptions } from "./components/ho
23
26
  import type { StatusLineComponent } from "./components/status-line";
24
27
  import type { ToolExecutionHandle } from "./components/tool-execution";
25
28
  import type { TranscriptContainer } from "./components/transcript-container";
29
+ import type { EventController } from "./controllers/event-controller";
26
30
  import type { LoopLimitRuntime } from "./loop-limit";
27
31
  import type { OAuthManualInputManager } from "./oauth-manual-input";
28
32
  import type { Theme } from "./theme/theme";
@@ -77,6 +81,18 @@ export interface InteractiveModeContext {
77
81
  statusLine: StatusLineComponent;
78
82
  session: AgentSession;
79
83
  sessionManager: SessionManager;
84
+ /** Session the transcript/editor/status are attached to: the focused agent's, else `session`. */
85
+ readonly viewSession: AgentSession;
86
+ /** Id of the focused agent, undefined when the main session is attached. */
87
+ readonly focusedAgentId: string | undefined;
88
+ /** Focus the main view on an agent's live session (delegates to SessionFocusController.focusAgent). */
89
+ focusAgentSession(id: string): Promise<void>;
90
+ /** Focus the focused agent's parent session, falling back to main (delegates to focusParent). */
91
+ focusParentSession(): Promise<void>;
92
+ /** Return the view to the main session (delegates to SessionFocusController.unfocus). */
93
+ unfocusSession(): Promise<void>;
94
+ /** Clear loader, status/pending containers, streaming state, and pending tools. */
95
+ clearTransientSessionUi(): void;
80
96
  settings: Settings;
81
97
  keybindings: KeybindingsManager;
82
98
  agent: AgentSession["agent"];
@@ -84,6 +100,10 @@ export interface InteractiveModeContext {
84
100
  mcpManager?: MCPManager;
85
101
  lspServers?: LspStartupServerInfo[];
86
102
  titleSystemPrompt?: string;
103
+ collabHost?: CollabHost;
104
+ collabGuest?: CollabGuestLink;
105
+ eventController: EventController;
106
+ eventBus?: EventBus;
87
107
  isInitialized: boolean;
88
108
  isBashMode: boolean;
89
109
  toolOutputExpanded: boolean;
@@ -341,6 +341,8 @@ export declare class AgentSession {
341
341
  getMnemopiSessionState(): MnemopiSessionState | undefined;
342
342
  /** TTSR manager for time-traveling stream rules */
343
343
  get ttsrManager(): TtsrManager | undefined;
344
+ /** Secret obfuscator, when secrets are configured; /share redaction reuses it. */
345
+ get obfuscator(): SecretObfuscator | undefined;
344
346
  /** Whether a TTSR abort is pending (stream was aborted to inject rules) */
345
347
  get isTtsrAbortPending(): boolean;
346
348
  /** Whether the plan-mode → compaction transition's expected internal abort is
@@ -385,6 +387,9 @@ export declare class AgentSession {
385
387
  */
386
388
  subscribe(listener: AgentSessionEventListener): () => void;
387
389
  subscribeCommandMetadataChanged(listener: CommandMetadataChangedListener): () => void;
390
+ /** True once dispose() has begun; deferred background work (e.g. the deferred
391
+ * MCP discovery task in sdk.ts) must not touch the session past this point. */
392
+ get isDisposed(): boolean;
388
393
  /**
389
394
  * Synchronously mark the session as disposing so new work is rejected
390
395
  * immediately: Python/eval starts throw, queued asides are dropped, and the
@@ -443,6 +448,14 @@ export declare class AgentSession {
443
448
  */
444
449
  getAllToolNames(): string[];
445
450
  isMCPDiscoveryEnabled(): boolean;
451
+ /**
452
+ * Flip MCP discovery on after deferred discovery learns the real tool count.
453
+ * UI sessions resolve `tools.discoveryMode: "auto"` before MCP servers
454
+ * connect, so a large MCP toolset discovered later must be able to upgrade
455
+ * the session from the force-activate path to the discovery path. One-way:
456
+ * discovery is never downgraded mid-session.
457
+ */
458
+ enableMCPDiscovery(): void;
446
459
  getSelectedMCPToolNames(): string[];
447
460
  activateDiscoveredMCPTools(toolNames: string[]): Promise<string[]>;
448
461
  isToolDiscoveryEnabled(): boolean;
@@ -24,8 +24,8 @@
24
24
  * eligibility off exact limit ids (`openai-codex:primary` /
25
25
  * `openai-codex:secondary`) and `usedFraction`, never off `status`.
26
26
  *
27
- * ANTI-WASTE GATES (in evaluation order): the policy must be OFF unless opted
28
- * in; the active model must be Codex (not Spark — a Spark block lives on a
27
+ * ANTI-WASTE GATES (in evaluation order): the policy must not be set to "no";
28
+ * the active model must be Codex (not Spark — a Spark block lives on a
29
29
  * separate meter and it is unknown whether a credit even resets it); a fresh
30
30
  * usage report for the active account must confirm `limitReached`; the WEEKLY
31
31
  * (secondary) window must be genuinely exhausted — a 5h-only block self-heals
@@ -38,6 +38,7 @@
38
38
  * read-only views are passed in so the predicate itself stays deterministic.
39
39
  */
40
40
  import type { OAuthAccountIdentity, ResetCreditTarget, UsageReport } from "@oh-my-pi/pi-ai";
41
+ import type { CodexAutoRedeemMode } from "../config/settings-schema";
41
42
  /** Weekly window counts as exhausted at `usedFraction >= 0.999` (used_percent >= 99.9). */
42
43
  export declare const WEEKLY_EXHAUSTED_MIN_FRACTION = 0.999;
43
44
  /** A weekly reset can never be more than one window length (7d) away; +1h slack for skew. */
@@ -48,6 +49,8 @@ export declare const REPORT_FRESHNESS_MS: number;
48
49
  export declare const ATTEMPT_COOLDOWN_MS = 60000;
49
50
  /** Minute bucket for blockKey, absorbing `reset_after_seconds`-derived jitter. */
50
51
  export declare const DEBOUNCE_BUCKET_MS = 60000;
52
+ export declare function shouldEvaluateCodexAutoRedeem(mode: CodexAutoRedeemMode): boolean;
53
+ export declare function shouldPromptCodexAutoRedeem(mode: CodexAutoRedeemMode): boolean;
51
54
  export type CodexAutoRedeemSkipReason = "disabled" | "wrong-provider" | "spark-model" | "no-identity" | "no-report" | "stale-report" | "not-limit-reached" | "weekly-not-exhausted" | "no-reset-time" | "reset-too-soon" | "reset-implausible" | "credits-unknown" | "reserve" | "already-attempted" | "cooldown";
52
55
  export interface CodexAutoRedeemInput {
53
56
  nowMs: number;
@@ -67,7 +70,7 @@ export interface CodexAutoRedeemInput {
67
70
  attemptedBlockKeys: ReadonlySet<string>;
68
71
  lastAttemptAtByAccount: ReadonlyMap<string, number>;
69
72
  }
70
- export type CodexAutoRedeemDecision = {
73
+ export interface CodexAutoRedeemRedeemDecision {
71
74
  redeem: true;
72
75
  target: ResetCreditTarget;
73
76
  accountKey: string;
@@ -75,7 +78,8 @@ export type CodexAutoRedeemDecision = {
75
78
  weeklyResetAtMs: number;
76
79
  remainingMs: number;
77
80
  availableCount: number;
78
- } | {
81
+ }
82
+ export type CodexAutoRedeemDecision = CodexAutoRedeemRedeemDecision | {
79
83
  redeem: false;
80
84
  reason: CodexAutoRedeemSkipReason;
81
85
  };
@@ -309,6 +309,12 @@ export declare class SessionManager {
309
309
  private sessionDir;
310
310
  private readonly persist;
311
311
  private readonly storage;
312
+ /**
313
+ * Collab replication tap: invoked for every appended entry with the
314
+ * in-memory (pre-blob-externalization) entry, so inline images survive.
315
+ * Failures are swallowed — a broadcast error must never break persistence.
316
+ */
317
+ onEntryAppended?: (entry: SessionEntry) => void;
312
318
  private constructor();
313
319
  /** Puts a binary blob into the blob store and returns the blob reference */
314
320
  putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult>;
@@ -434,6 +440,21 @@ export declare class SessionManager {
434
440
  */
435
441
  setSessionName(name: string, source?: "auto" | "user"): Promise<boolean>;
436
442
  _persist(entry: SessionEntry): void;
443
+ /**
444
+ * Append a foreign (host-authored) entry verbatim, preserving its
445
+ * `id`/`parentId` — no id minting. Used by collab guests to mirror the
446
+ * host session into the local replica file.
447
+ */
448
+ ingestReplicatedEntry(entry: SessionEntry): void;
449
+ /**
450
+ * Snapshot the session for collab replication: the live header plus a deep
451
+ * copy of every entry (the host mutates entries in place on
452
+ * truncation/rewrite paths, so guests must not share references).
453
+ */
454
+ snapshotForReplication(): {
455
+ header: SessionHeader;
456
+ entries: SessionEntry[];
457
+ };
437
458
  /** Append a message as child of current leaf, then advance leaf. Returns entry id.
438
459
  * Does not allow writing CompactionSummaryMessage and BranchSummaryMessage directly.
439
460
  * Reason: we want these to be top-level entries in the session, not message session entries,
@@ -24,14 +24,16 @@ export interface SnapcompactInlineOptions {
24
24
  }
25
25
  /** Tool-result swap candidate, in context order. */
26
26
  export interface InlineToolResultCandidate {
27
- /** toolCallId stable identity for render caching and application. */
27
+ /** Stable identifier for rendering cache key and applying the swap. */
28
28
  id: string;
29
- /** Token count of the joined text blocks (0 when empty or image-carrying). */
29
+ /** Token count of the joined text blocks (0 for empty or image-carrying). */
30
30
  textTokens: number;
31
- /** Frames needed to render the text (0 = empty or below the token floor). */
31
+ /** Frames needed to render the text (0 = empty, below floor, image-carrying, or error). */
32
32
  frames: number;
33
33
  /** Already carries an image (screenshot etc.) — never re-imaged. */
34
34
  hasImage: boolean;
35
+ /** Error tool results must stay text-only for provider API validation. */
36
+ isError?: boolean;
35
37
  }
36
38
  export interface InlineSystemPromptCandidate {
37
39
  textTokens: number;
@@ -73,6 +75,7 @@ export interface InlineMessageView {
73
75
  role: string;
74
76
  toolCallId?: string;
75
77
  content?: unknown;
78
+ isError?: boolean;
76
79
  }
77
80
  export interface SnapcompactSavingsEstimate {
78
81
  /** Frames only ship on models that accept image input. */
@@ -1,3 +1,4 @@
1
+ import type { AutocompleteItem } from "@oh-my-pi/pi-tui";
1
2
  import type { BuiltinSlashCommand, ParsedSlashCommand, SlashCommandResult, SlashCommandRuntime, SlashCommandSpec, TuiSlashCommandRuntime } from "./types";
2
3
  export type { BuiltinSlashCommand, SubcommandDef } from "./types";
3
4
  /** TUI-specific runtime accepted by `executeBuiltinSlashCommand`. */
@@ -5,6 +6,14 @@ export type BuiltinSlashCommandRuntime = TuiSlashCommandRuntime;
5
6
  export declare const BUILTIN_SLASH_COMMAND_RESERVED_NAMES: ReadonlySet<string>;
6
7
  /** Builtin command metadata used for slash-command autocomplete and help text. */
7
8
  export declare const BUILTIN_SLASH_COMMAND_DEFS: ReadonlyArray<BuiltinSlashCommand>;
9
+ /**
10
+ * Materialized builtin slash commands with completion functions derived from
11
+ * declarative subcommand/hint definitions.
12
+ */
13
+ export declare const BUILTIN_SLASH_COMMANDS: ReadonlyArray<BuiltinSlashCommand & {
14
+ getArgumentCompletions?: (prefix: string) => AutocompleteItem[] | null;
15
+ getInlineHint?: (argumentText: string) => string | null;
16
+ }>;
8
17
  /**
9
18
  * Unified registry exposed for cross-mode tooling. Each spec carries at least
10
19
  * one of `handle` / `handleTui`. The TUI dispatcher prefers `handleTui`; the
@@ -54,6 +54,13 @@ export interface ExecutorOptions {
54
54
  index: number;
55
55
  id: string;
56
56
  parentToolCallId?: string;
57
+ /**
58
+ * Spawn runs as a detached background job (parent turn not blocked on it).
59
+ * Rides the subagent lifecycle/progress payloads so HUD-style surfaces can
60
+ * skip spawns the transcript already renders inline. See
61
+ * {@link SubagentLifecyclePayload.detached}.
62
+ */
63
+ detached?: boolean;
57
64
  modelOverride?: string | string[];
58
65
  /**
59
66
  * Active model selector of the parent session, used as an auth-aware fallback
@@ -25,6 +25,8 @@ export interface SubagentProgressPayload {
25
25
  assignment?: string;
26
26
  progress: AgentProgress;
27
27
  sessionFile?: string;
28
+ /** See {@link SubagentLifecyclePayload.detached}. */
29
+ detached?: boolean;
28
30
  }
29
31
  /** Payload emitted on TASK_SUBAGENT_EVENT_CHANNEL */
30
32
  export interface SubagentEventPayload {
@@ -41,6 +43,13 @@ export interface SubagentLifecyclePayload {
41
43
  sessionFile?: string;
42
44
  parentToolCallId?: string;
43
45
  index: number;
46
+ /**
47
+ * Spawn runs as a detached background job: the parent turn keeps working
48
+ * while this agent runs. Sync task spawns (parent blocked on the call) and
49
+ * eval `agent()` bridge spawns (rendered inside their eval cell) leave this
50
+ * unset — surfaces like the subagent HUD only list detached spawns.
51
+ */
52
+ detached?: boolean;
44
53
  }
45
54
  export declare const taskItemSchema: z.ZodObject<{
46
55
  id: z.ZodOptional<z.ZodString>;
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": "15.11.7",
4
+ "version": "15.12.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",
@@ -40,23 +40,24 @@
40
40
  "fmt": "biome format --write . && bun run format-prompts",
41
41
  "format-prompts": "bun scripts/format-prompts.ts",
42
42
  "generate-docs-index": "bun scripts/generate-docs-index.ts",
43
- "prepack": "bun scripts/generate-docs-index.ts && bun scripts/bundle-dist.ts",
44
- "generate-template": "bun scripts/generate-template.ts"
43
+ "prepack": "bun scripts/generate-docs-index.ts && bun --cwd=../collab-web run build:tool-views && bun scripts/bundle-dist.ts",
44
+ "bench:guard": "bun scripts/bench-guard.ts"
45
45
  },
46
46
  "dependencies": {
47
47
  "@agentclientprotocol/sdk": "0.22.1",
48
48
  "@babel/parser": "^7.29.7",
49
49
  "@mozilla/readability": "^0.6.0",
50
- "@oh-my-pi/hashline": "15.11.7",
51
- "@oh-my-pi/omp-stats": "15.11.7",
52
- "@oh-my-pi/pi-agent-core": "15.11.7",
53
- "@oh-my-pi/pi-ai": "15.11.7",
54
- "@oh-my-pi/pi-catalog": "15.11.7",
55
- "@oh-my-pi/pi-mnemopi": "15.11.7",
56
- "@oh-my-pi/pi-natives": "15.11.7",
57
- "@oh-my-pi/pi-tui": "15.11.7",
58
- "@oh-my-pi/pi-utils": "15.11.7",
59
- "@oh-my-pi/snapcompact": "15.11.7",
50
+ "@oh-my-pi/hashline": "15.12.0",
51
+ "@oh-my-pi/omp-stats": "15.12.0",
52
+ "@oh-my-pi/pi-agent-core": "15.12.0",
53
+ "@oh-my-pi/pi-ai": "15.12.0",
54
+ "@oh-my-pi/pi-catalog": "15.12.0",
55
+ "@oh-my-pi/pi-mnemopi": "15.12.0",
56
+ "@oh-my-pi/pi-natives": "15.12.0",
57
+ "@oh-my-pi/pi-tui": "15.12.0",
58
+ "@oh-my-pi/pi-utils": "15.12.0",
59
+ "@oh-my-pi/pi-wire": "15.12.0",
60
+ "@oh-my-pi/snapcompact": "15.12.0",
60
61
  "@opentelemetry/api": "^1.9.1",
61
62
  "@opentelemetry/context-async-hooks": "^2.7.1",
62
63
  "@opentelemetry/exporter-trace-otlp-proto": "^0.218.0",
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Boot-time regression guard (Phase A1 of the boot/TUI perf work).
4
+ *
5
+ * Re-runs the `PI_TIMING=x` cold-boot benchmark under hyperfine and fails when
6
+ * the median regresses past `baseline * THRESHOLD`. `PI_TIMING=x` runs the full
7
+ * pre-paint chain in `runRootCommand` and then `process.exit(0)`, so the
8
+ * never-exiting interactive launch becomes a terminating, benchmarkable boot.
9
+ *
10
+ * Boot wall-clock is MACHINE-RELATIVE: a baseline captured on one machine is
11
+ * meaningless on another (and on CI). This is a LOCAL guard — regenerate the
12
+ * baseline on the machine you measure on, then compare on that same machine.
13
+ * It is intentionally NOT wired into CI for that reason.
14
+ *
15
+ * bun scripts/bench-guard.ts --update # capture/refresh the baseline
16
+ * bun scripts/bench-guard.ts # measure + compare; exit 1 on regression
17
+ *
18
+ * Requires `hyperfine` on PATH.
19
+ */
20
+ import * as fs from "node:fs";
21
+ import * as path from "node:path";
22
+
23
+ const THRESHOLD = 1.05; // 5% regression budget
24
+ const BASELINE_PATH = path.join(import.meta.dir, "..", "bench", "boot-baseline.json");
25
+ const BENCH_COMMAND = "PI_TIMING=x bun src/cli.ts";
26
+ const cwd = path.join(import.meta.dir, "..");
27
+
28
+ function medianOf(hyperfineJson: string): number {
29
+ const parsed = JSON.parse(hyperfineJson) as { results: Array<{ mean: number; median?: number }> };
30
+ const result = parsed.results[0];
31
+ if (!result) throw new Error("hyperfine produced no result");
32
+ return result.median ?? result.mean;
33
+ }
34
+
35
+ async function measure(): Promise<{ seconds: number; raw: string }> {
36
+ const tmp = path.join(import.meta.dir, "..", "bench", `.boot-run-${Date.now()}.json`);
37
+ const proc = Bun.spawn(["hyperfine", "--warmup", "3", "--min-runs", "10", "--export-json", tmp, BENCH_COMMAND], {
38
+ cwd,
39
+ stdout: "inherit",
40
+ stderr: "inherit",
41
+ });
42
+ const code = await proc.exited;
43
+ if (code !== 0) throw new Error(`hyperfine exited ${code}`);
44
+ const raw = await Bun.file(tmp).text();
45
+ fs.rmSync(tmp, { force: true });
46
+ return { seconds: medianOf(raw), raw };
47
+ }
48
+
49
+ const update = process.argv.includes("--update");
50
+ const { seconds, raw } = await measure();
51
+
52
+ if (update) {
53
+ fs.mkdirSync(path.dirname(BASELINE_PATH), { recursive: true });
54
+ await Bun.write(BASELINE_PATH, raw);
55
+ console.log(`Baseline updated: ${(seconds * 1000).toFixed(0)}ms median -> ${BASELINE_PATH}`);
56
+ process.exit(0);
57
+ }
58
+
59
+ if (!fs.existsSync(BASELINE_PATH)) {
60
+ console.error("No baseline found. Run `bun scripts/bench-guard.ts --update` on this machine first.");
61
+ process.exit(2);
62
+ }
63
+
64
+ const baseline = medianOf(await Bun.file(BASELINE_PATH).text());
65
+ const ratio = seconds / baseline;
66
+ const verdict = ratio > THRESHOLD ? "REGRESSION" : "ok";
67
+ console.log(
68
+ `boot median: ${(seconds * 1000).toFixed(0)}ms vs baseline ${(baseline * 1000).toFixed(0)}ms ` +
69
+ `(${((ratio - 1) * 100).toFixed(1)}%, budget ${((THRESHOLD - 1) * 100).toFixed(0)}%) -> ${verdict}`,
70
+ );
71
+ process.exit(ratio > THRESHOLD ? 1 : 0);
@@ -59,6 +59,10 @@ async function main(): Promise<void> {
59
59
  `process.env.PI_TINY_TRANSFORMERS_VERSION=${JSON.stringify(transformersVersion)}`,
60
60
  "--external",
61
61
  "mupdf",
62
+ "--external",
63
+ "fastembed",
64
+ "--external",
65
+ "onnxruntime-node",
62
66
  "--root",
63
67
  ".",
64
68
  "./packages/coding-agent/src/cli.ts",
@@ -72,6 +72,10 @@ async function main(): Promise<void> {
72
72
  "@oh-my-pi/pi-natives",
73
73
  "--external",
74
74
  "@huggingface/transformers",
75
+ "--external",
76
+ "fastembed",
77
+ "--external",
78
+ "onnxruntime-node",
75
79
  "--define",
76
80
  'process.env.PI_BUNDLED="true"',
77
81
  "./src/cli.ts",
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bun
2
+ /**
3
+ * Build the standalone share-viewer page the omp relay serves at `GET /s/<id>`.
4
+ *
5
+ * Same template as HTML exports, but with no embedded session: share-loader.js
6
+ * (injected right after the empty #session-data tag) fetches the sealed blob
7
+ * (gist or relay store), decrypts it with the `#<key>` fragment in-browser, and
8
+ * hands the JSON to template.js via `window.__OMP_SESSION_DATA__`.
9
+ *
10
+ * The relay repo's build script runs this and embeds the output via go:embed.
11
+ */
12
+ import * as path from "node:path";
13
+ import { generateThemeVars, getTemplate } from "../src/export/html";
14
+
15
+ const outPath = process.argv[2];
16
+ if (!outPath) {
17
+ console.error("usage: bun scripts/generate-share-viewer.ts <output.html>");
18
+ process.exit(2);
19
+ }
20
+
21
+ const loaderJs = await Bun.file(new URL("../src/export/html/share-loader.js", import.meta.url).pathname).text();
22
+ // Pin a built-in theme: the viewer is a public artifact, not a per-user export.
23
+ const themeVars = await generateThemeVars("dark");
24
+
25
+ const html = getTemplate()
26
+ .replace("<theme-vars/>", () => `<style>:root { ${themeVars} }</style>`)
27
+ .replace("<title>Session Export</title>", () => "<title>omp session</title>")
28
+ .replace("{{SESSION_DATA}}</script>", () => `</script>\n <script>${loaderJs}</script>`);
29
+
30
+ if (html.includes("{{SESSION_DATA}}")) throw new Error("session-data placeholder survived substitution");
31
+ if (!html.includes("__OMP_SESSION_DATA__")) throw new Error("share loader not injected");
32
+
33
+ await Bun.write(outPath, html);
34
+ console.log(`Generated ${path.resolve(outPath)} (${(html.length / 1024).toFixed(0)} KB)`);
package/src/cli/args.ts CHANGED
@@ -32,6 +32,8 @@ export interface Args {
32
32
  sessionDir?: string;
33
33
  providerSessionId?: string;
34
34
  fork?: string;
35
+ /** Collab link to join at startup (set by the `join` subcommand; no CLI flag). */
36
+ join?: string;
35
37
  models?: string[];
36
38
  tools?: string[];
37
39
  noTools?: boolean;
@@ -26,6 +26,7 @@ export const commands: CommandEntry[] = [
26
26
  { name: "gallery", load: () => import("./commands/gallery").then(m => m.default) },
27
27
  { name: "grievances", load: () => import("./commands/grievances").then(m => m.default) },
28
28
  { name: "install", load: () => import("./commands/install").then(m => m.default) },
29
+ { name: "join", load: () => import("./commands/join").then(m => m.default) },
29
30
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
30
31
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
31
32
  { name: "shell", load: () => import("./commands/shell").then(m => m.default) },