The extension shutdown context action installed by
ExtensionUiController.initializeHookRunner was an empty stub, so
ctx.shutdown() in interactive mode silently did nothing while extensions
fell back to process.exit(0), bypassing session flush and terminal
restore. Flip InteractiveModeContext.shutdownRequested so the main
loop's existing checkShutdownRequested() drives the graceful path.
Fixes#1020.
Slash skill invocations bypassed the Enter / Ctrl+Enter contract that
every other slash command honors. Now Enter steers, Ctrl+Enter queues
a follow-up, sharing one #invokeSkillCommand helper between the editor
submit handler and handleFollowUp. Compaction short-circuit ordering
preserved.
Closes#1033
Adds a new `rpc-ui` mode that extends the existing headless RPC mode with
interactive tool support (ask tool, extension UI dialogs, etc.).
In plain `rpc` mode the session has `hasUI=false` and no UI context is
wired, so interactive tools are disabled. `rpc-ui` mode sets `hasUI=true`
and wires a single shared `RpcExtensionUIContext` instance into both the
tool context store and the extension runner. Both consumers share the same
`pendingExtensionRequests` map and output closure, so `extension_ui_response`
messages received on stdin are routed to the correct waiting promise
regardless of which code path (tool or extension) created the request.
Changes:
- `args.ts`: add `rpc-ui` to the `Mode` union and the parse guard
- `launch.ts`: expose `rpc-ui` in the OCLIF flag definition and help text
- `main.ts`: propagate `rpc-ui` through all RPC-mode guard conditions and
pass `setToolUIContext` to `runRpcMode` when the mode is `rpc-ui`
- `rpc-mode.ts`: accept optional `setToolUIContext` callback; create one
shared `RpcExtensionUIContext` instance and pass it to both the tool
context store and the extension runner
Makes `/skill:<name> [args]` work identically under both submission
keybindings, mirroring how free text is already routed during streaming:
- `/skill:foo` + Enter, streaming -> steer queue (interrupt)
- `/skill:foo` + Ctrl+Enter, streaming -> followUp queue
- `/skill:foo` + Enter, idle -> idle prompt
- `/skill:foo` + Ctrl+Enter, idle -> idle prompt (was: literal text)
A single private helper `#invokeSkillCommand(text, streamingBehavior)`
on `InputController` handles the dispatch; the Enter submit handler
calls it with "steer", and `handleFollowUp` calls it with "followUp"
after the compaction short-circuit so a skill typed during compaction
rides the same `queueCompactionMessage` queue as free text.
Behavior deltas vs upstream/main:
- Enter on `/skill:foo` during streaming now steers (was: queued as
followUp). Users who relied on the followUp default can press
Ctrl+Enter -- the same key they already use for free-text follow-ups.
- Ctrl+Enter on `/skill:foo` is new capability; previously the
literal string `/skill:foo ...` was sent as plain followUp text and
the skill was never invoked.
Op: extend
A fifth ExitPlanMode approval choice — sits between the existing
"Approve and execute" (purge session) and "Approve and keep context"
(full transcript). Runs `handleCompactCommand` against the plan-mode
transcript with a planning-specific custom instruction rendered from
`plan-mode-compact-instructions.md`, then dispatches the plan-approved
synthetic prompt so it lands as the first entry in the freshly-
summarized transcript — giving execution a fresh cache anchor with
the rationale carried over.
Cancel/fail contract:
- ok → bookkeeping runs, plan-approved synthetic prompt dispatched.
- cancelled → bookkeeping runs (tools restored, plan reference path
recorded), warning surfaced, dispatch skipped.
`markPlanReferenceSent` is intentionally deferred past
the cancel guard so `AgentSession.#buildPlanReferenceMessage`
re-injects the plan on the operator's next prompt() call.
If we marked it sent on cancel, the executor's first turn
would have no plan context.
- failed → bookkeeping runs, error already surfaced by executeCompaction,
dispatch proceeds best-effort. Approval intent stands.
Cancel vs. fail is discriminated via `instanceof CompactionCancelledError`
at the session/compaction error boundary (introduced in the previous
commit), so any abort source — operator Esc, extension hook, programmatic
abort — classifies uniformly without input-modality or message-string
coupling.
Op: extend
Introduce `CompactionCancelledError` and `CompactionOutcome` ("ok" |
"cancelled" | "failed") so callers can discriminate user-driven aborts
from generic failures via `instanceof`, instead of inspecting error
messages or `AbortError`-name strings.
`AgentSession.compact()`'s two abort-rejection sites now throw the
typed sentinel; the model-call wrapper normalizes AbortError-shaped
rejections to the sentinel only when the compaction's abort signal
is actually set, preserving every other exception unchanged so real
compaction bugs are not silently relabeled as cancellations.
`CommandController.executeCompaction` and `handleCompactCommand`
return `Promise<CompactionOutcome>`; the catch classifies via
`instanceof CompactionCancelledError`. Existing callers (`/compact`,
loop runner, auto-compact) ignore the return value — non-breaking.
Op: extend
- Added conflictCount metadata and warning badge output to read results for files with unresolved conflicts.
- Added conflict detection parsing with strict marker matching and session-scoped conflict IDs.
- Added write-path conflict resolution for `conflict://N` using token expansion and marker validation before splicing.
- Added unit and integration tests for conflict scanning, history lifecycle, URI validation, and workflows.
- Replaced Python execution with a local `python -u runner.py` subprocess and NDJSON stdin/stdout framing.
- Removed shared-gateway architecture, including coordinator lifecycle APIs, `useSharedGateway` wiring, and `jupyter` CLI/actions.
- Simplified setup checks to a plain Python 3 availability probe and removed automatic dependency-install fallbacks.
- Updated kernel cancellation and display processing to use status frames, SIGINT/SIGTERM escalation, and normalized output coercion.
- Added `python-runner` integration and display tests while deleting legacy websocket and kernel lifecycle test suites.
- Updated addMessageToChat in UI helpers and InteractiveModeContext to return rendered components instead of void.
- EventController now tracks IRC message components and removes them after a 10-second TTL, avoiding duplicate expiry scheduling per message signature.
- EventController dispose now clears all pending IRC expiry timers and tests were added for immediate render, TTL removal, duplicate suppression, and timer cleanup.
- Added edit streaming fallback data to the edit render context and used it when no finalized patch text was available.
- Implemented hashline fallback rendering that strips envelope syntax, sanitizes text, and truncates output to a fixed number of lines with a streaming indicator.
- Updated tool execution to populate the fallback preview from the active edit strategy and added a renderer test for hashline envelope input while the diff is not yet computable.
The HTML export feed, sidebar tree, and TUI session tree did not handle
the developer role, so plan content injected after /plan approval (and
any other developer messages) was hidden from /export and shown as a
bare [developer] label in /tree.
Renders developer messages in the main feed with a dimmed
.developer-message style, labels them in the sidebar tree, counts them
in header stats, and shows their content in the TUI tree selector so
search matches the body.
Closes#753.
Co-Authored-By: omp <noreply@oh-my-pi.dev>
- Trim logo from 14w to 12w (2-wide legs).
- Diagonal BL→TR gradient instead of per-line LTR.
- Truecolor: 3-stop magenta→violet→cyan path that skips the deep-blue valley.
- 256-color: same 6-stop ramp as fallback.
- Compute the colored logo once at module load instead of per render.
- Added asynchronous Kitty conversion for assistant tool images using `convertToPng`, keyed per tool-call entry with cached and in-flight tracking.
- Updated assistant image rendering to prefer converted PNGs for Kitty terminals while preserving existing behavior for other protocols.
- Added a unit test that verifies WebP tool images are converted and rendered as Kitty image output instead of the raw image/webp fallback.
The server-side OAuthCallbackFlow callback window is 300_000 ms, but the
client starts its #send timer before the RPC command is dispatched. By
the time the callback server actually starts, some time has already been
consumed on the client side. If the user takes the full callback window
plus token exchange, the client 300_000 ms timeout fires first, rejects
login(), cleans up the onOpenUrl listener, and the server response
arrives into a discarded pending entry.
Use 600_000 ms (10 min) on the client — double the server window. The
server will always return an error or success response within its own
window, so this timeout is purely a safety net against server crash or
connection loss and never fires during a normal login.
Replace the static RPC_LOGIN_PROVIDERS allowlist with runtime detection
based on callback ordering.
Providers that support headless login (OAuthCallbackFlow) always call
onAuth first (emit the browser URL), then onManualCodeInput only as a
fallback for manual redirect-URL entry. Providers that require interactive
input (API-key paste, device-flow prompts, GitHub Enterprise URL) call
onPrompt before any onAuth fires.
onPrompt now branches on authEmitted:
- false (onPrompt before onAuth): reject immediately with a clear 'not
supported in RPC mode' error. The rejection propagates through
authStorage.login and is caught by the try/catch, returning an error
response. No deadlock.
- true (onPrompt after onAuth, inside OAuthCallbackFlow's fallback race):
return a never-settling promise so the race defers to the callback
server. A rejection here would be swallowed as null by .catch() and
spin the while(true) loop.
New providers self-classify by their actual call order with no list to
maintain.
OAuthCallbackFlow.#waitForCallback races the browser callback against
onManualCodeInput and catches any rejection as null. When onPrompt
threw, the catch turned it into null, the while(true) loop saw a
falsy result, and immediately re-invoked onManualCodeInput — a tight
spin that starved the callback server and made browser-callback logins
hang until timeout even when the user completed auth successfully.
Replace the throw with a never-resolving Promise<string>. The race
then blocks waiting for the callback server to deliver the code,
with no busy-looping. When the server-side login eventually
times out or the browser callback arrives, the pending promise is
abandoned and GC'd.
Addresses: https://github.com/can1357/oh-my-pi/pull/987#discussion_r3213450206
#send uses a hard-coded 30s timeout. OAuth flows require the user
to open a browser, authenticate with the provider, and wait for the
redirect — easily 1-3 minutes. Callers would see a spurious timeout
rejection while the server-side callback server was still live and
the user was still mid-flow.
Add optional timeoutMs parameter to #send (default 30_000, all
existing callers unaffected) and pass 300_000 from login().
Addresses: https://github.com/can1357/oh-my-pi/pull/987#discussion_r3213435093
RpcClient#handleLine was dropping extension_ui_request frames
(no isAgentEvent match → early return). Callers of login() had
no way to learn the auth URL, so the callback-server flow never
got a browser visit and the command hung until timeout.
- Add isRpcExtensionUiRequest type guard
- Add #extensionUiListeners set to RpcClient
- Dispatch extension_ui_request frames through that set in #handleLine
- login() accepts optional { onOpenUrl } callback; registers/
deregisters a listener scoped to the command's lifetime
Addresses: https://github.com/can1357/oh-my-pi/pull/987#discussion_r3213428270
- RpcCommand: add get_login_providers and login { providerId }
- RpcResponse: add typed responses for both commands
- RpcExtensionUIRequest: add open_url { url, instructions } event
(fire-and-forget; host opens the URL in system browser)
- rpc-mode: handle get_login_providers (returns provider list with
per-provider authenticated status) and login (drives authStorage.login
using RpcExtensionUIContext for onPrompt dialogs and notify for
onProgress; emits open_url for the auth page URL)
- rpc-client: add getLoginProviders() and login(providerId) methods
Anthropic counts sessions by metadata.user_id. Without this fix, OMP
generated fresh random entropy on every API request, inflating the
session count and preventing backend attribution to the authenticated
account.
Changes:
packages/ai:
- resolveAnthropicMetadataUserId() now accepts JSON-format user_id
matching real Claude Code's getAPIMetadata shape
({ session_id, account_uuid, ... }). Previously only the legacy
cloaking format was accepted on OAuth, causing stable caller-supplied
values to be silently discarded.
- AnthropicOAuthFlow.exchangeToken() and refreshAnthropicToken() now
populate OAuthCredentials.{accountId, email} from the token response
account block, removing the need for a separate /api/oauth/profile
round-trip.
- AuthStorage.getOAuthAccountId(provider, sessionId) returns the OAuth
accountId for the session-sticky credential, used to build
account_uuid in metadata.user_id. Guards against misattribution for
API-key, runtime-override, env-key, and fallback-resolver paths that
do not record a session credential.
packages/agent:
- Agent.metadataForProvider(provider) resolves request metadata for
the given provider via the installed resolver, or returns the static
metadata value. The plain metadata getter now returns only the static
value; provider-aware resolution is explicit.
- Agent.setMetadataResolver(fn) installs a (provider: string) resolver
evaluated per LLM request in agent-loop, after getApiKey records the
session-sticky credential, so account_uuid reflects the credential
actually used.
- AgentLoopConfig.metadataResolver is called with config.model.provider
after getApiKey, overriding the static metadata field.
packages/coding-agent:
- AgentSession.#syncAgentSessionId installs a metadata resolver that
builds { user_id: JSON.stringify({ session_id, account_uuid? }) },
matching the Anthropic session attribution format. account_uuid is
only included for provider="anthropic" to avoid leaking the OAuth
identity to third-party Anthropic-format-compatible providers.
- sessionId getter prefers providerSessionId when supplied via
AgentSessionConfig so all API paths (getApiKey, direct calls,
metadata resolver) share the same provider-facing session ID.
- prepareSimpleStreamOptions stamps session metadata on direct calls
(runEphemeralTurn, compaction, branch summary, title generation) so
they share the same session bucket as Agent.prompt requests.
- generateBranchSummary and generateSessionTitle accept a
(provider: string) metadata resolver evaluated after their own
getApiKey call for correct credential attribution.
- Updated ExtensionUIContext, InteractiveModeContext, and InteractiveMode to require editor factories to return CustomEditor instances.
- Removed the runtime compatibility guard and warning for non-CustomEditor implementations in setEditorComponent.
- Removed the test that verified rejection of non-CustomEditor factories in interactive-mode editor-component tests.
- Render each exit_plan_mode submission as a fresh plan review entry so refined plans are emitted into terminal scrollback.
- Keep external-editor edits replacing the current preview instead of adding duplicate review entries.
- Updated the plan review regression test to preserve the first preview and append the second one after intervening chat content.
Add an explicit openai-models-list discovery type for custom providers while keeping lm-studio as a compatible alias. Custom providers can now point baseUrl at an OpenAI-compatible /v1 endpoint, provide an api key, and auto-populate models via GET {baseUrl}/models.
Discovered models merge cleanly with user-defined models from models.yml/models.json, so YAML entries still win for display name and token limits. The provider picker now explains when discovery succeeds but returns zero models, and when the configured /models endpoint responds with 404.
Fixes#970
Interactive /mcp test only consulted getMCPConfigPath("user"|"project")
configs and missed servers defined in standalone .mcp.json. /mcp reauth
already resolved through #findConfiguredServer, which includes that
fallback path. Route /mcp test through the same resolver so both
commands enumerate the same set of servers.
Fixes#956
The custom status line rendered cache_read with the input icon and
cache_write with the output icon — backwards relative to Anthropic's
cache_creation_input_tokens (write) / cache_read_input_tokens (read)
semantics. Swap the icon pairings in status-line/segments.ts and the
labels in status-line-segment-editor.ts to match.
Fixes#953
- Added hideThinkingSummary options across stream, agent, and session payload paths.
- Routed Coding-Agent hideThinkingBlock toggles to agent hideThinkingSummary during session updates.
- Updated OpenAI, Azure OpenAI, and Codex requests to omit reasoning.summary when hide/ summary is null.
- Reworked system-prompt preparation with per-step timeouts, fallback defaults, and step-level warnings.
- Added optional `/loop` `count|duration` command arguments and wired `command.args` into loop handling.
- Implemented `loop-limit` parsing and runtime types/helpers for iteration and duration budget limits with validation.
- Updated interactive mode to enforce loop limits per iteration, check duration expiry, and clear budget state on disable.
- Added loop-limit parse/runtime tests and fixed `/loop` arg errors plus macOS `MallocStackLogging` environment leakage.
- Exported hashline section interfaces and helpers, including a new section-diff API for external callers.
- Refactored `computeHashlineDiff` to split input sections, propagate split errors, and delegate each section via helper.
- Added context-highlight caching and batched highlighting for unchanged lines, with `replaceTabs` fallback on unknown files.
- Fixed hashline streaming preview so completed sections stay visible when a new `@PATH` header appears mid-stream.
- Added hashline streaming tests for section persistence, malformed trailing `+ 7`, and dual sections.
- Move planModeEnabled check before plan.enabled guard so /plan can
still exit an active session even if setting toggled off mid-session
- Clear stale plan/plan_paused mode_change entries when plan.enabled
is off during session restore, preventing unexpected re-entry when
the setting is later re-enabled
- Converted systemPrompt APIs and state types to ordered `string[]` across agent, AI, and coding-agent surfaces.
- Added `normalizeSystemPrompts` and applied it to context normalization before building provider request payloads.
- Updated AI providers to emit separate normalized prompt blocks/messages instead of a single merged system prompt.
- Removed dedicated `projectPrompt` state and remapped that context into system-context buckets in session, dump, and token accounting.
- Aligned tests and changelogs to pass and assert `systemPrompt` as arrays with ordered prompt semantics.
- Added a new `edit.hashlineAutoDropPureInsertDuplicates` boolean setting with default `false` for hashline edits.
- Threaded the setting through tool execution contexts so hashline previews and execution honor the configured option.
- Changed pure-insert duplicate boundary absorption to run only when enabled and added tests for default-disabled and enabled behavior.
- Updated read schema, path utilities, and dispatch to parse selectors from :raw/:L suffixes on path, removing standalone sel usage.
- Changed truncation/error notices to continue with :<nextOffset> and :1 guidance for read and sqlite pagination.
- Added summarizeCode support with tree-sitter summaries, read.summarize settings, and N-API Summary types/exports.
- Added tests and docs updates for path-embedded selectors, summary behavior, and explicit raw/offset SQL/read cases.
- Added session-draft persistence methods in SessionManager to write unsent editor text to an artifacts-sidecar draft file and delete it after single-shot consumption.
- Persisted editor text during interactive shutdown and restored that draft on resume when the editor was empty, enabling Ctrl+D draft recovery.
- Updated Ctrl+D handling in the editor/controller path and added tests covering draft round-trip, artifact cleanup, stale-draft eviction, and in-memory no-op behavior.