Commit Graph

1742 Commits

Author SHA1 Message Date
Xisheng Parker Zhao
4e28b0941b fix(server): surface Kiro CLI slash commands and skills over ACP (#1792)
* fix(server): surface Kiro CLI slash commands and skills over ACP

Kiro CLI advertises its slash commands and skills through the
`_kiro.dev/commands/available` ACP extension notification rather than the
standard `available_commands_update` session update. ACPAgentSession only
trace-logged extension notifications and dropped the payload, so the
composer never showed any Kiro commands or skills.

- Map the extension payload onto the shared slash-command cache:
  `commands` (built-in, names arrive with a leading "/") and `prompts`
  (skills/prompts, tagged with a `skill:` serverName) are normalized to
  Paseo's AgentSlashCommand shape, stripping the leading slash and pulling
  the argument hint from `meta.hint`.
- Add KiroACPAgentClient (mirrors CursorACPAgentClient) so the provider
  waits for the first async command batch before listCommands() resolves,
  since Kiro emits the notification shortly after session/new.

* fix(server): settle on empty Kiro batch; inject Kiro commands via constructor option

Addresses review feedback on #1792.

Keep vendor-specific providers out of the generic acp-agent.ts, following
the same pattern as Cursor/Copilot: provider behavior is injected through
ACPAgentClient constructor options rather than special-cased in the base
class. Adds an optional extensionCommandsParser option on the generic ACP
client/session; the base extNotification() invokes it and routes any parsed
commands through a generic applyResolvedCommands() helper. Kiro supplies
parseKiroExtensionCommands (recognizes _kiro.dev/commands/available); the
base class no longer references any Kiro method string or payload shape.

Also fixes an empty-batch hang: applyResolvedCommands() always settles the
initial-commands gate, so a Kiro user with no slash commands no longer
blocks listCommands() for the full 10s timeout. Adds a regression test.
2026-06-30 09:36:48 +02:00
Mohamed Boudra
8eb9640f28 chore(release): cut 0.1.102-beta.2 2026-06-30 00:02:06 +02:00
Mohamed Boudra
cf2fccd01d Keep agent lists working when project records go stale (#1812)
* fix(server): keep agent fetch RPCs resilient

Agent directory projection assumed every workspace still had a parent project. A stale workspace/project record could reject the whole fetch_agents RPC, so missing placement now behaves like no placement and direct fetches can still return the agent.

* test(server): cover stale agent history fetches
2026-06-29 23:00:40 +02:00
Mohamed Boudra
811f52395e Fix Windows image previews (#1811)
* fix(images): preserve Windows preview paths

Provider image markdown used escaped backslash paths on Windows, which markdown parsing could turn into encoded drive paths that the app treated as workspace-relative. Emit drive-letter paths as file URIs and normalize existing encoded local paths before file preview.

* test(server): accept file URI image sources

Windows provider image output now renders drive-letter paths as file URIs. Decode those markdown sources before filesystem assertions so the image rendering tests keep checking the materialized file.

* fix(images): keep plain local paths literal

Limit legacy URI decoding to markdown-encoded Windows drive paths so ordinary local paths with percent sequences remain unchanged. Document why provider image markdown matching still accepts old doubled-backslash history.
2026-06-29 20:49:13 +00:00
Mohamed Boudra
f31bc2b8b7 Fix pushed state for checked-out PR worktrees (#1804)
* fix(worktree): track checkout PR branches correctly

Checkout-PR worktrees could point branch tracking at a PR push remote without fetching a matching remote-tracking ref. Configure same-repo PRs to track origin, make fork PR remotes fetchable before assigning upstream, and push fork PR branches to their configured remote.

* fix(worktree): scope PR push routing to Paseo targets

Review caught that using ordinary upstream config for Push could send normal branches to non-origin remotes and mishandle deduped PR branch names. Store a Paseo-specific push target during PR worktree creation, then have Push honor only that marker before falling back to origin.

* fix(worktree): use git upstream for PR pushes

* fix(worktree): allow PR checkout without head upstream

* fix(worktree): preserve fork PR git push target

* fix(worktree): preserve PR push targets

* fix(worktree): preserve PR push status

* fix(worktree): set upstream after push-only PR push
2026-06-29 20:21:19 +02:00
Mohamed Boudra
a1afdfee51 Fix Claude subagent narration leaking into chat (#1807)
* fix(claude): keep subagent text out of parent transcript

Sidechain tool activity is folded into the parent Task/Agent row, but sidechain assistant text was still going through the top-level text assembler. That made the chat show subagent narration without its matching tool rows.

* test(claude): simplify subagent sidechain assertion
2026-06-29 19:40:15 +02:00
Mohamed Boudra
85acaceb16 Fork assistant turns into new drafts (#1788)
* feat(chat): fork assistant turns into new drafts

Builds chat-history attachments on the daemon and routes them into draft composers so users can continue an assistant turn in an existing workspace tab or New Workspace.

* fix(chat): clean up fork draft handling

* fix(chat): address fork draft review feedback

* fix(server): build fork context from bounded timeline

* fix(app): restore assistant turn footer in streams

* fix(app): preserve fork draft setup

* fix(app): tighten assistant fork footers

* fix(app): lock active fork draft handoff

* fix(app): preserve attachment-only draft retries

* fix(app): unify composer attachment scopes

* fix(app): align chat history attachment labels

* fix(app): centralize attachment pill content

* fix(app): trim fork context for workspace naming
2026-06-29 18:29:59 +02:00
Mohamed Boudra
e63a971968 Make daemon shutdowns easier to diagnose (#1790)
* fix(daemon): log stop reasons and client identity

Record websocket client identity, process memory/uptime, and shutdown reasons across CLI, desktop, supervisor, and worker paths so daemon drops can be traced from the triggering client to worker termination.

* fix(daemon): keep shutdown diagnostics in sync

Test drift: supervisor and relay tests still asserted the old log text and metadata shape after shutdown diagnostics started logging structured reasons and relay connection ids. Update those assertions to the new diagnostic contract.

Also centralize client lifecycle reason normalization and derive desktop daemon stop reasons from one tuple so future changes cannot silently drift.
2026-06-29 23:31:11 +08:00
Mohamed Boudra
b613bea9f6 Default client RPC waits to 60 seconds (#1789)
* fix(client): wait longer for session responses

* fix(client): default RPC waits to sixty seconds

* fix(app): preserve detached stream scroll on delayed history

Code drift: longer client RPC waits let delayed timeline responses arrive after a user scroll-away, so web stream anchoring must not reattach on transient scroll-top resets.

Restore the 15s connect deadline and leave app initialization slack above the default 60s session RPC wait.

* fix(client): keep helper waits within caller deadlines

Review fix: the 60s default session RPC wait leaked into wait previews and waitForAgentUpsert helper fetches. Bound those helper RPCs to the caller deadline or a short best-effort preview timeout, and allow small scroll ranges to reattach at bottom.

* fix(client): respect caller timeout budgets

* fix(cli): keep diagnostic probes responsive

* Refactor daemon client request options

* Preserve daemon client legacy overloads
2026-06-29 17:23:18 +02:00
agamotto
6a23bbd121 Fix Windows OpenCode process cleanup 2026-06-28 19:11:22 +07:00
Mohamed Boudra
3288e1cfb9 Stop agent prompts renaming existing workspaces 2026-06-28 19:10:59 +07:00
Mohamed Boudra
821d194fbe fix: allow Docker web UI loopback aliases 2026-06-27 09:28:34 +00:00
Mohamed Boudra
ab3ed56513 fix: repair beta Docker image 2026-06-27 09:09:52 +00:00
Mohamed Boudra
c7e27fdd5b chore(release): cut 0.1.102-beta.1 2026-06-27 05:08:20 +00:00
Slava Goltser
24a1f1b8b9 feat(pi): make extension result timeout configurable via provider params and increase default to 30s (#1732)
* feat(pi): make extension result timeout configurable via provider params

* fix(acp): add tests asserting cwd and mcpServers are always passed to session/load (#1593)

Add unit tests in acp-agent.test.ts that verify initializeResumedSession()
always calls loadSession and unstable_resumeSession with { sessionId, cwd,
mcpServers } — even when mcpServers is an empty array. Some strict ACP
providers (e.g., Devin CLI) return 'Invalid params' if any of these fields
are omitted.

Also adds a docstring above initializeResumedSession() documenting this
requirement so future refactors don't accidentally drop params.

Closes #1593

* fix(acp): address Greptile review on session/load invariant tests

- Extract shared makeTestSession() factory to eliminate duplicated TestSession
  class definitions across all three tests (concern #1)
- Pass handle through typed constructor option instead of casting private
  initialHandle field (concern #2)
- Add missing type imports for AgentCapabilityFlags and AgentPersistenceHandle

* fix(tests): ensure ACP agent session load invariant

* fix(pi): increase default extension result timeout to 30s
2026-06-27 02:53:45 +00:00
Hu Sheng
f275275074 fix(server): use terminateWithTreeKill in Claude Code provider close() (#1540)
* fix(server): use terminateWithTreeKill in Claude Code provider close()

The Claude Code agent's close() method only called SDK-level cleanup
(query.close/interrupt/return) which may only kill the direct child
process. MCP server processes spawned by the Claude CLI survived as
orphans, accumulating over time and consuming significant memory.

Fix:
- Add onChildProcess callback to ClaudeQueryContext to capture the
  spawned child process reference
- Store the child process in ClaudeAgentSession
- In close(), call terminateWithTreeKill() after SDK cleanup to
  recursively terminate the entire process tree (claude + MCP children)
- Use 2s graceful (SIGTERM) / 2s force (SIGKILL) timeouts, consistent
  with other providers (ACP, Codex, Pi, OpenCode)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix(server): tree-kill old process during query restart to prevent MCP orphans

Greptile review identified a gap: ensureQuery() replaces this.childProcess
when spawning a new query without first killing the old process tree.
During reconnection events, this can leak MCP children from the
previous claude process.

Fix: add terminateWithTreeKill() in the queryRestartNeeded block
after SDK cleanup, before the replacement query spawns.

Co-Authored-By: Claude <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-06-26 22:02:25 +08:00
rex
5ef118ea33 fix(opencode): prevent indexing the entire home directory (#1704)
* fix(opencode): prevent indexing the entire home directory

Paseo launches opencode serve with cwd=os.homedir() and refreshes the
global provider snapshot with directory=/Users/admin. OpenCode treats
that as a workspace and starts location services + bigram indexing for
the entire home tree, causing ~466% CPU and ~4GB RAM usage.

- Use a neutral scratch directory as the opencode serve cwd.
- Use a separate scratch directory for global provider catalog refresh
  so model/mode discovery no longer triggers home directory indexing.

Fixes high CPU/RAM when Paseo starts opencode with no explicit project.

* fix(opencode): use realpath-aware matcher for home detection in catalog refresh

Switch the home-directory check in fetchCatalog from a string-based
path.resolve() comparison to createRealpathAwarePathMatcher, so we
catch macOS /private/var/... aliases, symlinks, trailing separators,
and Windows casing — consistent with the rest of opencode-agent.ts.

Also:
- Hoist the matcher to module scope so each fetchCatalog call doesn't
  rebuild it (the matcher runs realpathSync twice on construction).
- Log a debug line when we rewrite the cwd to the scratch path, so
  it's easy to diagnose missing per-directory config in catalog scope.
- Update opencode-agent.test.ts to expect the scratch directory when
  cwd === os.homedir(), with a comment pointing to the rationale.

* fix(opencode): isolate helper server home

* fix(opencode): pass semantic global catalog scope

* fix(opencode): release catalog acquisition on home resolution failure

---------

Co-authored-by: rex-chang <rex-chang@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-06-26 21:55:34 +08:00
Éverton Toffanetto
99643ad085 fix(server): no-op agent hooks when PASEO_TERMINAL_ID is unset (#1667)
* fix(server): no-op agent hooks when PASEO_TERMINAL_ID is unset

The shell command generated by `buildAgentHookShellCommand` short-circuited
on `[ -n "$PASEO_TERMINAL_ID" ] && ...` and exited 1 when the terminal
ID was absent, so Claude Code reported every Notification/Stop/StopFailure/
SessionEnd/UserPromptSubmit event as `Failed with non-blocking status code`.
The same shape affected the Codex provider.

Switch the gate to `if [ -n ... ]; then ...; fi` so the hook is a true
no-op when the terminal id is unset and preserves the underlying CLI's
exit code when it actually runs. Mirrors `opencode-plugin.ts`, which
already early-returns on the same env var. Adds the matching
`else (exit /b 0)` branch to `buildAgentHookWindowsCommand`.

Updates the literal-string assertions in the existing Claude and Codex
hook tests, and adds an executable regression that runs the generated
shell command through `/bin/sh` and asserts exit 0 across all five
Claude hook events.

* test(server): skip POSIX hook regression on Windows

The /bin/sh exit-code regression spawns /bin/sh -c <posix command>,
which has no equivalent on the Windows runner, so spawnSync returned
status: null and the five Claude events failed in CI.

Gate the it.each block with skipIf(isPlatform("win32")), matching the
pattern already used in worker-terminal-manager.test.ts and
executable-resolution.test.ts. The remaining literal-string and
buildTerminalEnvironment assertions in this file keep running on
every platform.
2026-06-26 16:38:25 +08:00
Davy
40265782d4 feat: add remote daemon self-update from client (#1513)
* feat: add remote daemon self-update from client

Adds a daemon.update.request RPC that lets the client trigger a remote
daemon update over WebSocket. The daemon runs npm update -g @getpaseo/cli,
reports progress via daemon_update_progress status events, then restarts
through the supervisor to load the new code.

New feature flag server_info.features.daemonSelfUpdate gates the UI.
The UpdateDaemonCard appears on the host settings page when the daemon
supports self-update and the app/daemon versions differ.

* fix: clean up progress subscription on unmount, use typed phase cast

Address Greptile review:
- P1: store unsubscribe ref and clean up on component unmount to prevent
  listener leak during in-flight update
- P2: use DaemonUpdateProgressStatusPayload type instead of { phase?: string }

* fix: use npm install@latest and global concurrency guard

Address Greptile review:
- npm update -g can silently exit 0 without resolving major-version
  gaps; switch to npm install -g @getpaseo/cli@latest
- Move daemonUpdateInProgress from per-session instance to module-level
  variable so concurrent clients share the same guard

* feat(daemon): harden remote self-update

Probe the global npm CLI install before updating, keep request handling in a daemon self-update session controller, and emit scoped daemon.update.progress messages.

* test(daemon): make self-update paths platform-stable

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-06-26 16:13:29 +08:00
Matteo Pietro Dazzi
862154541a feat: add MiniMax quota fetcher and brand icon (#1662)
* feat(server): add MiniMax quota fetcher

Mirror the live multi-provider quota panel (#1278) for MiniMax. Resolves the
bearer token from $MINIMAX_API_KEY or the MiniMax CLI config at
~/.mmx/config.json (api_key, oauth.access_token) and credentials at
~/.mmx/credentials.json, then queries the /v1/token_plan/remains endpoint on
the configured region (global by default, cn when region='cn'). The response
is normalized into ProviderUsage windows per model_remains entry, surfacing
the interval and weekly limits with their reset times and a danger tone when
the server reports status=2.

* feat(app): add MiniMax brand icon for usage page

The quota panel resolves the icon via resolveProviderIconName, which falls
back to a generic Bot for any provider not in BUILTIN_PROVIDER_ICON_NAMES.
Register 'minimax' as a built-in icon and vendor the official 24×24 brand
mark (single Path, currentColor) as MiniMaxIcon so the usage card stops
showing the generic robot.

* refactor(server): flatten nested ternaries in MiniMax quota fetcher

Greptile review on #1662 called out two nested ternaries in toIntervalWindow
and toWeeklyWindow. Extract a toneForStatus helper that uses an explicit
if/else chain so the same status-to-tone mapping is shared between the
interval and weekly windows, and add the missing trailing newline.

* fix: format
2026-06-26 14:14:29 +08:00
Mohamed Boudra
8c3b709794 fix(server): honor X-Forwarded-Proto so daemon web UI auto-connects behind HTTPS reverse proxy (#1739)
* fix(server): honor forwarded proto for daemon web UI

* fix(server): configure trusted daemon proxies

* docs: document daemon web UI reverse proxy setup

* docs: consolidate web UI proxy guidance
2026-06-26 13:26:50 +08:00
Mohamed Boudra
4e2c06ec71 Serve the web client from the daemon (#1635)
* Serve the web client from the daemon

Keep the bundled browser UI opt-in and exclude it from desktop packaging so desktop builds do not ship a duplicate renderer.

* Escape daemon web UI bootstrap hint

* Fix bundled web UI dist path
2026-06-26 12:00:11 +08:00
Mohamed Boudra
bbde200aa2 chore(release): cut 0.1.101 2026-06-26 00:19:37 +07:00
Mohamed Boudra
26f169866f Add app diagnostic report (#1728)
* feat(diagnostics): add app diagnostic report

* fix(diagnostics): guard app diagnostic runs

* fix(diagnostics): include websocket runtime metrics

* fix(diagnostics): type websocket metric snapshot

* fix(diagnostics): include daemon shell env

* fix(loop): make stop cancellation test deterministic
2026-06-25 23:20:52 +08:00
Mohamed Boudra
bdd9419189 Speed up new Pi agent startup (#1727)
* fix(providers): avoid draft feature model guessing

* fix(providers): avoid draft command model guessing
2026-06-25 22:18:58 +08:00
Mohamed Boudra
f12c9e9cfa fix(voice): scope OpenAI voice credentials
Resolve OpenAI voice credentials and endpoints from voice-specific config before broader OpenAI fallbacks, and use the same REST STT provider for dictation and voice mode.
2026-06-25 18:41:57 +07:00
Mohamed Boudra
2cf041c4d8 Keep provider diagnostics useful when discovery is slow (#1724) 2026-06-25 19:30:19 +08:00
Mohamed Boudra
3b5e34fe12 Keep liveness pings responsive during slow requests (#1723) 2026-06-25 19:16:54 +08:00
Mohamed Boudra
eaecd8d0a7 Prepare providers for direct Paseo tools (#1707)
* refactor(agent): decouple Paseo tools from MCP

Keep MCP as the fallback adapter while exposing the same runtime catalog for providers that can register tools directly.

* fix(protocol): keep native tool support server-internal

* fix(i18n): sync Japanese shortcut labels

* fix(agent): preserve schedule provider error
2026-06-25 13:20:51 +08:00
Mohamed Boudra
f9ff668a71 Link worktrees to PRs from differently named tracked branches (#1718)
* fix(server): link PRs for differently named tracked branches

PR lookup now uses the configured tracked head ref when it differs from the local branch, while keeping fork owner scoping only for cross-repo remotes.

* fix(server): avoid extra PR lookup git reads

Use the shared lookup-target builder for an early local-branch result before reading origin and base metadata on on-demand PR status refreshes.
2026-06-25 05:05:32 +00:00
Mohamed Boudra
b561b108a7 refactor(server): extract the workspace-scripts feature into a deep module (#1716)
* refactor(server): extract the workspace-scripts feature into a deep module

The service-proxy-backed "workspace scripts" feature had no home: building a
workspace's scripts payload was duplicated between the descriptor builder and
buildWorkspaceScriptPayloadSnapshot (the same 9-input buildWorkspaceScriptPayloads
assembly), and the "scripts available on this daemon?" guard appeared a third time
in the start handler.

Move the feature into session/workspace-scripts/workspace-scripts-service.ts behind
createWorkspaceScriptsService(deps): { buildSnapshot, emitStatusUpdate, start }. The
payload assembly and availability guard now live in one place; the descriptor builder,
the status-emission path, and the start RPC all funnel through it. spawnWorkspaceScript
is injected as a port so the feature is testable without a real process. session.ts
drops ~98 lines; the two methods existing tests reach stay as thin delegators.

New zero-mock unit test covers the guard, status emission, and the start branch matrix
with injected fakes, the real service proxy + runtime store, and a fake launcher.

* refactor(server): address review — durable comment + documented test stand-in

- Drop the transient "#1714" PR reference from the buildWorkspaceScriptPayloadSnapshot
  accessor comment; state the durable reason instead.
- Hoist the opaque terminalManager test stand-in to a named const documenting the
  non-call guarantee, rather than an inline `as unknown as` cast.
2026-06-25 12:20:55 +08:00
Mohamed Boudra
0be5cc9dae refactor(server): extract the agent-update subscription stream into a deep module (#1715)
Move the per-client `agent_update` subscription out of session.ts into
session/agent-updates/ behind createAgentUpdatesService(deps). The module owns
the mutable subscription state, the bootstrap buffer, the provider-visibility
gate, and the filter predicate; the rest of session.ts no longer pokes the
subscription shape or hand-rolls `agent_update` payloads — the ~12 call sites
collapse to thin delegations (forwardLiveAgent / emitStoredRecord / removeAgent /
beginSubscription / flushBootstrapped / clearSubscription / dispose) plus the
pure, shared matchesAgentUpdatesFilter used by the snapshot listing pager.

The shared payload builders (buildAgentPayload / buildStoredAgentPayload /
isProviderVisibleToClient) and buildProjectPlacementForWorkspaceId stay in
session.ts and are injected as host callbacks, since they are used widely
outside this cluster. session.ts: 6187 -> 5967.

Behavior preserved: the existing session.workspaces.test.ts integration suite is
repointed through the new boundary and stays green; the previously-untested
filter / buffer / flush branches are now covered by a zero-mock unit test with
injected fakes. cleanup() now disposes the subscription (it was never cleared
before). The clear-attention-for-workspace path that emits agent_update directly
(bypassing the buffer) is left as-is — a pre-existing inconsistency, out of scope.
2026-06-25 12:20:46 +08:00
Mohamed Boudra
e4f32a4d82 refactor(server): extract workspace git-observer into a deep module (#1714)
* refactor(server): extract workspace git-observer into a deep module

Move the workspace git-observer cluster out of session.ts into
session/workspace-git-observer/ behind createWorkspaceGitObserverService(deps).
The module owns the per-cwd watch targets and the WorkspaceGitService
subscription handles, so the registration / dedupe / branch-change / teardown
lifecycle lives in one place instead of being reached into by hand from the
emit hot-path, the archive path, CheckoutSession's host, and dispose.

session.ts: 6401 -> 6219 lines. The caller surface narrows: archive teardown
calls removeForWorkspaceId / removeForCwd (no longer resolving watch targets and
cwds itself), and dispose() replaces iterating a raw subscription Map.

Sheds dead code orphaned by 40e27f5a0 (FSWatcher watching moved into
WorkspaceGitService): the workspaceGitFetchSubscriptions Map (never written), the
WorkspaceGitWatchTarget watchers/debounceTimer/refreshPromise/refreshQueued
fields, and the no-op closeWorkspaceGitWatchTarget method.

The module is unit-tested with injected fakes (zero mocks); the existing
session.workspace-git-watch.test.ts integration suite is repointed through the
new boundary and still passes.

Behavior is preserved verbatim, including a latent quirk where syncObservers
seeds descriptor state with a directory in the workspaceId slot (an effective
no-op) -- flagged as a follow-up, not changed here.

* refactor(server): clear watch targets on dispose; document shouldSkipUpdate dedupe

Addresses Greptile review on PR #1714:
- dispose() now clears watchTargets alongside subscriptions so post-teardown
  state is consistent (dispose is terminal; behavior-preserving).
- Document shouldSkipUpdate as a check-and-record dedupe gate (mutation on the
  changed path is intentional, preserved from the original).

* test(server): make git-observer test paths platform-portable

server-tests (windows-latest) failed: the test asserted raw POSIX path literals
against the service's resolve(cwd)-normalized output, which becomes D:\repo\ws1
on Windows. Resolve the test cwds the same way so assertions hold on every
platform (matches the path.resolve pattern in session.workspace-git-watch.test).
Test-only fix; production normalization is unchanged and correct.
2026-06-25 12:20:38 +08:00
Mohamed Boudra
b625b69302 Render images from Claude Code tool results in chat (#1717)
* feat(providers): render images Claude Code emits instead of base64

Claude returns images as base64 blocks inside tool_result content; they were
dumped into the tool output as a wall of raw base64 text. Materialize them to a
temp file and emit an assistant_message markdown image — the path Codex already
uses — so the client fetches the bytes over the existing WebSocket binary-frame
channel. No protocol or client change; assistant_message is an existing type.

Also strips the base64 out of the failed-tool-call error field, and widens the
Claude history-replay filter so the image survives reload. Collapses the image
writer Codex had duplicated twice into one shared materializeProviderImage.

* fix(providers): match only materialized image paths in the history filter

The history-replay filter recognized a provider image by a leading "![". Review
flagged that user-authored text starting with an image-markdown pointing at the
attachments dir could then replay as assistant output. Tighten the predicate to
the content-hashed <sha256>.<ext> shape the writer actually emits, so only
materialized images pass.

* fix(providers): handle Windows paths in the image-markdown predicate

On Windows the materialized image path uses backslash separators, and
escapeMarkdownImageSource doubles each backslash in the markdown source. The
history-replay predicate matched a single separator, so on a Windows daemon the
image was dropped on reload; the test path assertions failed for the same
reason. Allow one-or-more separators in the predicate, unescape the source in
the test helper, and add a Windows-path case to the predicate test.
2026-06-25 04:11:46 +00:00
Mohamed Boudra
1b9543023e refactor(server): extract workspace provisioning into a deep module (#1712)
* refactor(server): extract workspace provisioning into workspace-provisioning module

Move the "find-or-create a workspace & project for a directory" cluster out of
the 6.4k-line Session god class into session/workspace-provisioning/
behind an injected createWorkspaceProvisioningService(deps) port.

Nine methods (~215 LOC) — findOrCreateWorkspaceForDirectory,
resolveOrCreateWorkspaceIdForCreateAgent, createWorkspaceForDirectory,
findOrCreateProjectForDirectory, ensureWorkspaceRecordUnarchived, and the
private helpers resolveWorkspaceDirectory / findExactWorkspaceByDirectory /
reclassifyOrUnarchiveWorkspaceForDirectory / resolveProjectRecordForPlacement —
now live in one deep module with a 5-method interface. The session shed four
pure-function imports it no longer needs and delegates from five call sites.

The module's only dependencies are the workspace/project registries and the
git-service checkout port — no emit, clientActivity, or observer coupling — so
it is now unit-testable with real file-backed registries and a fake git port
(workspace-provisioning-service.test.ts), instead of only through a booted
Session. Behavior is unchanged: the existing open_project / import / create-agent
characterization suites stay green.

* refactor(server): address review — drop redundant spread property, test always-create

- Remove the dead `workspaceId: input.workspace.workspaceId` in
  reclassifyOrUnarchiveWorkspaceForDirectory (already spread via
  ...input.workspace; behavior-identical).
- Add a direct test pinning createWorkspaceForDirectory's always-mint-fresh
  contract (call twice for the same cwd → two distinct workspace ids), the
  property that distinguishes it from findOrCreateWorkspaceForDirectory.
2026-06-25 03:36:14 +08:00
Mohamed Boudra
83123987d7 refactor(server): extract git-mutation primitives into git-mutation module (#1711)
* refactor(server): extract git-mutation primitives into git-mutation module

Move checkoutExistingBranch, createBranchFromBase and notifyGitMutation
(plus their internal ref-validation / clean-tree / branch-existence helpers)
out of the 6.5k-line Session class into
session/git-mutation/git-mutation-service.ts — a deep module with a
3-method interface and a createGitMutationService(deps) factory, mirroring
the git-metadata-generator port extracted in #1702.

These primitives were smeared across three sub-session boundaries as host
callbacks. CheckoutSession now takes gitMutation directly, shrinking
CheckoutSessionHost from 6 members to 4; the worktree session-config
builder and the auto-naming / worktree-creation paths call
this.gitMutation.* instead of private Session methods.

Adds git-mutation-service.test.ts: guard branches covered with in-memory
fakes, happy paths against a real temp git repo. The superseded
internal-mock tests in session.test.ts (which stubbed execCommand and the
git service) are removed in favour of the real-dependency coverage.

* refactor(server): address review on git-mutation-service

- Inline doesLocalBranchExist into its sole caller: once the redundant
  ref-validation is removed (createBranchFromBase already validates
  newBranchName with the "new branch" label), the helper is a pure
  passthrough to workspaceGitService.hasLocalBranch.
- Replace .some(...).toBe(true) snapshot assertions with toContainEqual so
  failures surface the full recorded call (and assert cwd too).
2026-06-25 01:01:40 +08:00
Mohamed Boudra
45a7a91768 Improve workspace names for slash-command prompts (#1709)
* fix(server): mark metadata prompts as naming input

* fix(app): add Japanese agent mode shortcut label

* fix(server): keep mock metadata prompt detection current

* test(app): retry e2e temp workspace cleanup
2026-06-25 00:00:43 +08:00
Mohamed Boudra
484ffc4334 refactor(server): extract git-metadata generators into checkout module (#1702)
The LLM-backed commit-message and PR-text generators lived in the 6.6k-line
session god-file but were consumed only by CheckoutSession, which reached them
through two host callbacks (the host comment even flagged that it "does not own
them"). The two methods were ~80% identical.

Lift them into session/checkout/git-metadata-generator.ts as a deep module with
a two-method interface, collapsing the duplicated diff -> fileList -> patch ->
prompt -> generate -> fallback scaffold. The LLM call is now an injected
StructuredTextGeneration port: production wires resolve-providers + structured
generation; the new unit test injects a fake and exercises success, both
fallback paths, and error rethrow without mocking any module.

CheckoutSession takes the generator as a typed collaborator instead of two host
callbacks; Session sheds ~160 lines plus five now-unused imports. Behaviour is
unchanged — the existing session.test.ts generation tests pass untouched.
2026-06-24 23:48:00 +08:00
Mohamed Boudra
119afd7281 chore(release): cut 0.1.100 2026-06-24 17:42:47 +07:00
Mohamed Boudra
c5442ef0a2 Stop Claude context meter from doubling requests (#1701)
* fix(claude): stop probing context usage after turns

Use Claude stream/result usage for parent context tracking, and update the meter from compact boundary post tokens. Subagent usage reports stay out of the parent context meter.

* fix(claude): clear compact context fallback between turns
2026-06-24 17:44:39 +08:00
Mohamed Boudra
0748149ec9 feat(providers): expose custom agent selection (#1700) 2026-06-24 16:39:48 +08:00
Mohamed Boudra
2ef119c24b Fix OMP slash commands and skills loading (#1698)
* Fix OMP slash command discovery

Keep Pi on get_commands while allowing the OMP-backed provider to use get_available_commands. Both providers reject the other command without echoing an id, so the adapter needs an explicit command-list RPC setting instead of a timeout-based fallback.

* Address OMP command test review
2026-06-24 15:11:05 +08:00
Mohamed Boudra
79be6d8dba Stop OpenCode helper servers from leaking (#1697)
* Fix OpenCode helper server ownership

Own OpenCode helper generations as soon as spawn returns so startup timeout and shutdown clean up through the manager/reaper path. Collapse the OpenCode runtime wrapper so the provider talks to the server manager directly.

* Protect dedicated OpenCode server startup
2026-06-24 14:44:43 +08:00
Mohamed Boudra
7a817774b7 chore(release): cut 0.1.99 2026-06-23 22:47:51 +07:00
Mohamed Boudra
f94b488c4f Fix ACP user message echo dedupe 2026-06-23 22:25:16 +07:00
Mohamed Boudra
1970a14349 Keep provider diagnostics and model lists in sync (#1660)
* fix(server): align provider catalog diagnostics

* fix(server): preserve catalog profile models

* refactor: unify provider catalog discovery under AgentClient.fetchCatalog

Remove listModels/listModes from AgentClient and fetchModels/fetchModes
from ProviderDefinition. All provider runtime discovery now flows through
a single fetchCatalog(options) => ProviderCatalog API.

ProviderSnapshotManager.listModels/listModes remain as cached snapshot
conveniences only. Provider implementations (acp, codex, opencode, pi,
claude, mock) updated accordingly; agent-manager default model resolution
now calls fetchCatalog.

Reshape step toward issue pi-model-list-empty.

* refactor: remove remaining provider listModels/listModes runtime API residue

Migrate remaining AgentClient/provider-client implementations and tests to
fetchCatalog. Remove obsolete ListModelsOptions/ListModesOptions interfaces.
Update ProviderSnapshotManager.getProviderDiagnostic to materialize clients
via ensureClient(provider, definition) so diagnostics self-heal the settings
sheet instead of failing when providerClients[provider] is absent.

Allowed to remain: ProviderSnapshotManager.listModels/listModes as cached
snapshot readers; protocol/client legacy list_provider_models names; unrelated
local helper in create-agent-mode.

* test(server): repair test clients after fetchCatalog refactor

- Restore TestAgentClient.fetchCatalog with proper model list and resumeSession.
- Restore NativeArchiveRecordingClient and EnvProbeAgentClient removed during refactor.
- Fix ResumeCaptureClient.fetchCatalog and resumeSession.
- Fix stream-coalescing TestAgentClient.fetchCatalog shape and isAvailable.
- Mock accessible OpenCode provider in full-access mode tests so fetchCatalog does not throw.

* refactor(app): update stable discovered models ref directly during render
2026-06-23 17:55:47 +08:00
Mohamed Boudra
26b2f25050 refactor(server): decompose session.ts into per-domain subsystems (#1646)
* refactor(server): move checkout write handlers into CheckoutSession

The checkout read side moved into session/checkout/checkout-session.ts in
#1644; the 17 inline write/PR/stash handlers stayed on the Session god-object.
This carves them into the same subsystem behind an expanded CheckoutSessionHost,
so dispatchCheckoutMessage becomes pure delegation and session.ts drops ~800
lines.

Moved into CheckoutSession (verbatim, only this.X -> this.host.X rewiring):
switch/rename branch, commit, merge, merge-from-base, pull, push, PR
create/merge, github auto-merge/check-details, PR status/timeline, github
search, stash save/pop/list, plus resolveCurrentPullRequest and the
PR-timeline helpers.

CheckoutSessionHost gains the Session-owned, non-checkout collaborators these
handlers orchestrate (notifyGitMutation, emitWorkspaceUpdateForCwd,
handleWorkspaceGitBranchSnapshot, renameCurrentBranch, checkoutExistingBranch,
and the LLM commit/PR-text generators); paseoHome/worktreesRoot join the
options bag. GitMutationRefreshReason moves to utils/checkout-git.ts so both
the shell and the subsystem share one canonical type.

session.test.ts checkout tests now drive the handlers through the public
handleMessage boundary instead of reaching into private methods; new mock-free
fake-host tests cover the moved handlers directly in checkout-session.test.ts.

Behavior-preserving: full session.test.ts + checkout-session.test.ts green,
typecheck/lint/format clean.

* refactor(server): extract chat/schedule/loop handlers into ChatScheduleLoopSession

Move the 7 chat/*, 9 schedule/*, and 5 loop/* request handlers, the three
rpc-error emitters (kept separate), and toScheduleSummary out of the Session
god-object into session/chat/chat-schedule-loop-session.ts, mirroring the shipped
CheckoutSession/VoiceSession deep-module-with-narrow-Host pattern. The least-coupled
remaining domain: stateless request/response over chatService/scheduleService/
loopService, reaching the shell only through a narrow ChatScheduleLoopSessionHost
(emit + agent-roster reads + the mention-fanout send). session.ts drops ~590 lines.

The two dispatch switches collapse into one: schedule/* was previously reached only
via the chat dispatcher's default fall-through arm — now all 21 types are explicit
delegation cases, removing that fragility.

Behavior-preserving: a new routing test drives the real Session.handleMessage for all
21 types (guards against a silently-dropped case), green before and after the move; a
collocated subsystem test covers the mention-fanout send seam, the fanout-limit error
code, the self->agent target remap, and the toScheduleSummary runs-stripping.

* refactor(server): extract provider catalog handlers into ProviderCatalogSession

Move the 8 provider-catalog handlers (model/mode/feature listing, providers
snapshot pull/refresh, diagnostic, usage) plus emitProviderDisabledResponse,
getProviderSnapshotEntryForRead, buildDraftAgentSessionConfig, the two mode-icon
downgrade helpers, and the providers_snapshot_update PUSH wiring out of session.ts
into session/provider/provider-catalog-session.ts behind a narrow
ProviderCatalogSessionHost seam — the same deep-module pattern as ChatScheduleLoopSession,
CheckoutSession, and VoiceSession.

The PUSH (start) and every PULL gate provider visibility and downgrade mode icons
through the SAME injected predicates (isProviderVisibleToClient + supportsCustomModeIcons),
both reading appVersion/clientCapabilities live — the COMPAT invariant the shell could
only enforce by code proximity before. dispatchProviderMessage collapses to delegation;
start()/dispose() wire into subscribeToOptionalManagers/cleanup at the same ordinal.

session.ts drops ~372 lines. Behavior-preservation gate (provider routing through the
real Session.handleMessage) green before and after; full session.test.ts 115/115; new
collocated subsystem test covers the PUSH/PULL parity seam, the disabled-provider path,
and the usage/feature error envelopes.

* refactor(server): extract workspace file-access handlers into WorkspaceFilesSession

Move the file-explorer, file-upload, file-transfer-frame, project-icon and
file-download-token handlers out of session.ts into
session/files/workspace-files-session.ts behind a narrow WorkspaceFilesSessionHost
seam (emit/emitBinary/hasBinaryChannel), matching the deep-module-with-Host shape
used by CheckoutSession/ChatScheduleLoopSession/ProviderCatalogSession.

The new module owns the FileUploadStore and is injected the daemon's
DownloadTokenStore; the shell's dispatch cases and the file_transfer binary route
collapse to delegation. The five handlers touch no workspace-git observer,
registry or subscription state, so this is a clean self-contained slice of the
workspace domain. session.ts drops 240 lines (7316 -> 7076).

Behavior-preservation tests (file explorer list/read/binary, download token
success + empty-cwd, project icon, upload round-trip) added to session.test.ts and
run green against the pre- and post-refactor Session; a collocated
workspace-files-session.test.ts exercises the module via a fake host with real
upload/download stores (no module mocks).

* refactor(server): extract agent-config setters into AgentConfigSession

Move the four agent-config setters (set_agent_mode/model/feature/thinking)
out of the session god-object into session/agent-config/, collapsing their
four near-identical try/log/emit envelopes into one applyConfigChange helper
behind a narrow { emit } host plus an AgentConfigOperations port.

Behavior-preserving: the dispatch cases delegate unchanged, setAgentModeCommand
still routes the mode path, and model/feature still emit no notice. Adds the
previously-absent failure-envelope coverage: 8 gate tests in session.test.ts
(success + forced failure per setter, asserting the *_response envelope and the
activity_log-before-response ordering) green before and after the carve, plus a
collocated agent-config-session.test.ts (typed fake operations + fake host, no
module mocks). session.ts drops ~173 lines.

* refactor(server): extract project-config handlers into ProjectConfigSession

Move read/write_project_config handlers, their failure emitters, and the
root-resolution helpers (resolveKnownProjectRootForConfig, canonicalizeConfigRoot,
stripTrailingPathSeparators) out of session.ts into session/project-config/, behind
a narrow { emit } host plus a projectRegistry port — matching the established
session/<domain>/ pattern. Also corrects a misfiling: project-config was dispatched
inside dispatchAgentConfigMessage despite being its own domain.

session.ts drops 155 lines (6903 -> 6748). The handleMessage-driven gate in
session.test.ts (project config RPC authorization) is green before and after; new
collocated project-config-session.test.ts covers the subsystem with a fake host and
fake registry over a real filesystem (no module mocks).

* refactor(server): extract daemon status/pairing handlers into DaemonSession

Move the two daemon.* RPC handlers (daemon.get_status, daemon.get_pairing_offer)
out of session.ts into session/daemon/daemon-session.ts behind a narrow
{ emit } host + injected reads (paseoHome, serverId, daemonVersion,
daemonRuntimeConfig, listProviderAvailability). These were misfiled inside
dispatchAgentConfigMessage despite being daemon- not agent-scoped; the slice
also removes the now-dead serverId/daemonVersion/daemonRuntimeConfig fields and
the getPidLockInfo/generateLocalPairingOffer imports from the god-file, and
collapses the inline daemonRuntimeConfig type into a named DaemonRuntimeConfig.

The daemon-config get/set handlers stay inline: daemonConfigStore is a
listener-bearing, cross-domain hub (read by structured-generation,
websocket-server, bootstrap, archive-if-safe), so moving it would double-own it.

session.ts: 6748 -> 6662 lines. Gate: new session.test.ts daemon round-trips
(status success + listing-rejects fallback, pairing relay-disabled) pass before
and after; new collocated daemon-session.test.ts (fake host + injected reads
over a real temp dir, no module mocks) 4/4. typecheck/lint/format clean.
2026-06-22 10:44:07 +08:00
Mohamed Boudra
2b5cc727f0 refactor(server): move VoiceSession into session/voice/ to match CheckoutSession (#1645)
#1640 extracted the voice subsystem to server/voice/ (a peer of session.ts);
#1644 extracted the checkout read side to session/checkout/. The two carves
landed in inconsistent homes, so there was no single place to find "what has
session.ts been carved into."

Relocate server/voice/ -> server/session/voice/ so all session subsystems share
one home alongside session/checkout/. Pure relocation: git-tracked renames with
import-depth bumped one level (../ -> ../../); the only external touch point is
the import path in session.ts. Behavior preserved verbatim — the 17 voice unit
tests pass unchanged before and after.

Also record the actual shipped layout and the next carve (the checkout mutation
handlers still inline in session.ts -> session/checkout/checkout-session.ts) in
the decomposition plan, and fix its now-stale voice path.
2026-06-21 20:45:26 +08:00
Mohamed Boudra
18f880e561 refactor(server): extract checkout read subsystem into CheckoutSession (#1644)
Extract the checkout read & live-stream side (status, branch validate/suggest, manual refresh, diff + status subscriptions) out of session.ts into session/checkout/checkout-session.ts behind a narrow CheckoutSessionHost seam and a CheckoutDiffSubscriber port. session.ts -234 net lines; 12 unit tests via injected fakes (no module mocks); 6 session tests moved onto the public handleMessage path. Also fixes a pre-existing tilde-resolution bug in the refresh handler (TDD).
2026-06-21 19:37:48 +08:00
Mohamed Boudra
617cf8a7bf refactor(server): extract voice mode subsystem into VoiceSession (#1640)
* refactor(server): extract voice mode subsystem into VoiceSession

session.ts was a 10.5k-line god object and the most-churned file in the
repo. ~1,100 of those lines were an entire voice/audio subsystem — the
STT/TTS/dictation managers, the barge-in audio-buffering state machine,
voice-turn orchestration, and the MCP voice bridge — interleaved field by
field and method by method with workspace/git/agent/provider concerns.

Move the whole subsystem into a new VoiceSession deep module. Session now
holds one `voiceSession` field, constructs it once, and delegates the
voice/dictation/abort message types, the permission auto-allow gate, and
cleanup to it. VoiceSession owns all 18 voice fields and ~25 methods and
reaches back into the agent run only through a narrow VoiceSessionHost
seam (emit, loadAgent, reloadAgentSession, sendSpokenInput,
interruptAgentIfRunning, hasActiveAgentRun).

Behavior is preserved (verbatim method moves); deleting voice-session.ts
now removes the feature mechanically. The voice unit tests drive the
VoiceSession boundary instead of reaching into Session internals, so
future voice tests can construct a VoiceSession with a fake host.

session.ts: 10,468 -> 9,272 lines.

* refactor(server): move VoiceSession into voice/ and test it at the boundary

Address Greptile review on #1640:

- Move voice-session.ts into the existing voice/ subdirectory, alongside
  voice-turn-controller.ts, instead of adding another peer to the 30+ file
  server/ directory. The directory now carries the domain.
- Add voice/voice-session.test.ts beside the module: it constructs a
  VoiceSession with a fake VoiceSessionHost and drives it through the public
  API (handleSetVoiceMode, then turn-detection/STT events), asserting on the
  host seam (sendSpokenInput) and emitted messages. No Session, no private
  field/method access.
- Remove the voice tests from session.test.ts that reached through Session's
  private voiceSession field into VoiceSession internals.

Same three behaviors are covered (streaming final -> agent, finalization
timeout empty path, low-confidence filtering); driving handleSetVoiceMode
additionally exercises the enable path the old tests bypassed.
2026-06-21 07:14:23 +00:00