Commit Graph
321 Commits
Author SHA1 Message Date
SmallSpiderandGitHub 3eac3456b4 Fix Codex subagent final result preservation (#602) 2026-05-09 08:58:31 +08:00
weishu 752a505973 Release version 0.17.4 2026-05-08 19:12:59 +08:00
weishu 2fe1a2ed45 fix(cli): trust injected Codex session hook
Codex now requires hook trust before non-managed hooks can run. HAPI relies on the runtime-injected SessionStart hook to receive the Codex thread/session id, so leaving that hook untrusted breaks local Codex startup without manual /hooks review.\n\nGenerate the same trusted_hash Codex derives for the injected SessionStart command and pass it through the runtime hooks.state override. The trust is scoped to the synthetic session-flags hook key and the exact generated command, so user, project, and plugin hooks still go through Codex review normally.\n\nAlso cover the generated config args so future changes keep both the hook declaration and its trust state together.\n\nValidation:\n- bun test cli/src/codex/utils/codexMcpConfig.test.ts\n- bun typecheck
2026-05-08 19:09:38 +08:00
SmallSpiderandGitHub 293f944724 fix: omit reasoning summary for codex spark subagents (#594) 2026-05-07 16:44:56 +08:00
Xing WangandGitHub b17269d9e9 fix(cli): match claude code 2.x AskUserQuestion answer shape (#579)
The built-in AskUserQuestion tool in claude code 2.x reads
`updatedInput.answers` keyed by the question text and expects each
value to be a single string (multi-select selections joined by
commas), then echoes them verbatim from
`mapToolResultToToolResultBlockParam`. HAPI was filling `answers` as
`Record<questionIndex, string[]>`, so claude's lookup
`answers[question.question]` missed every entry, every question fell
through to `(no option selected)`, the filter dropped them all, and
the tool result content arrived as

    "User has answered your questions: . You can now continue with
     the user's answers in mind."

— a sentence with no answers. Sessions appeared to hang after the
user clicked an option, because claude had nothing actionable to act
on and never produced another turn.

Walk `input.questions` and rebuild the answers map keyed by the
question text, joining multi-select selections with commas. The
codex `request_user_input` path keeps its existing nested-shape
builder.

Verified end-to-end: re-spawned a HAPI session with the patched
binary, asked claude to use AskUserQuestion, picked options in the
web UI, and the turn resumed normally with answers visible in the
tool result content.
2026-05-07 10:47:19 +08:00
SmallSpiderandGitHub 841b7cc035 Add Codex multi-agent timeline support (#588)
* checkpoint codex multiagent UI state

* fix codex multiagent event scoping

* fix: stabilize codex subagent timeline

* chore: remove codex subagent nesting prompt

* test(web): stabilize tool result rendering tests

* fix: collapse codex agent trace rows by default

* fix(web): keep chat scrolled to bottom

* fix(web): backfill agent-run-heavy message loads

* fix(codex): fail stuck subagent spawns

* fix(codex): preserve wrapped child event scope

* fix(codex): surface agent tool completions
2026-05-07 10:46:16 +08:00
junesandGitHub 0006d04f9e feat: support multiple workspace roots (#584) 2026-05-07 08:30:52 +08:00
Junmo KimandGitHub e17d7e5995 fix(web): align Agent tool dialog with TUI ctrl+o expand (#585) 2026-05-07 08:28:03 +08:00
xiaobaifly7andGitHub 6df84df756 fix(hapi): consolidate approved web and Codex recovery fixes (#578) 2026-05-06 20:02:46 +08:00
Junmo KimandGitHub 8185f0287e feat(web,hub): cancel queued messages (#568) 2026-05-06 13:32:45 +08:00
Junmo KimandGitHub de69027926 fix(acp): hoist Gemini edit/write content into Claude-shaped input (#575) 2026-05-06 13:31:25 +08:00
3ef4c27ee5 fix(cli): hide Codex app-server console window on Windows (#574)
Spawn codex with windowsHide on win32 to avoid extra cmd windows
and stray exit code 128 shells when using shell: true.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-06 10:30:56 +08:00
weishu 0db303d9f8 Fix release lockfile platform packages 2026-05-06 09:40:43 +08:00
weishu 47c408c84f Release version 0.17.3 2026-05-06 09:32:21 +08:00
f7a40bd573 feat(web): polish chat rendering and fix remote session interactions (#567)
* feat(web): polish chat rendering

Refresh the web chat presentation across user messages, tool cards, code blocks, diffs, reasoning, and Mermaid diagrams.\n\nAdd focused regression coverage for bubble/status behavior, code and diff rendering, clipboard output, Mermaid theming, and message-window updates.

* fix(web): stabilize chat tool rendering

Preserve manual scroll anchors while older messages and tool dialogs update, and align code, diff, and tool result rendering with chat typography.

Add chat font-weight settings, ignore local Playwright CLI artifacts, document the Angular commit-message convention, and cover the scroll, result, code, diff, and settings behavior with focused tests.

Constraint: User requested committing all current workspace diffs with Angular-style commit messaging

Tested: bun run typecheck:web && bun run test:web && git diff --check

Co-authored-by: OmX <omx@oh-my-codex.dev>

* style(tool-card): polish question and permission card styles

Align AskUserQuestion option surfaces and permission action hierarchy with the existing tool card visual language while preserving interaction logic. Extract shared option presentation helpers and theme-driven hover/muted colors to reduce duplication.

Constraint: Frontend style-only polish; preserve existing permission and answer submission behavior

Rejected: Keep screenshot artifacts in the repo | they are local visual review output, not source

Confidence: high

Scope-risk: narrow

Tested: git diff --check; bun run typecheck:web; bun run test:web; bun run build:web

Not-tested: manual cross-browser visual QA beyond local Playwright inspection

Co-authored-by: OmX <omx@oh-my-codex.dev>

* fix(cli): keep Claude remote plan prompts actionable

Handle Claude remote /plan locally so HAPI switches plan permission mode before forwarding any prompt text. This avoids Claude Code treating /plan as an unknown skill and ending with only a ready event.

Constraint: Claude SDK result messages are not conversation log entries, so command handling must happen before the prompt reaches Claude.\nRejected: surfacing SDK result summaries in web chat | would expose transport-level summaries broadly instead of fixing the slash-command path.\nConfidence: high\nScope-risk: narrow\nDirective: Keep Claude remote slash commands that alter runtime mode in the CLI special-command parser.\nTested: bun test cli/src/parsers/specialCommands.test.ts; bun typecheck; git diff --check\nNot-tested: Manual GitHub-hosted runner deployment.

* fix(web): polish tool result rendering

* fix(web): preserve collapsed session order

* fix(chat): settle initial thread scroll

* fix(settings): remove chat font weight option

* fix(web): remove font weight bootstrap code

* chore: remove unrelated branch artifacts

* test(web): update consumed message invocation test

* fix(chat): cancel initial scroll settling on manual scroll

---------

Co-authored-by: huhaoyu.hahahu <huhaoyu.hahahu@bytedance.com>
Co-authored-by: OmX <omx@oh-my-codex.dev>
2026-05-06 09:18:27 +08:00
junesandGitHub 23c0fa4872 fix(cli): hide Windows taskkill popups during process cleanup (#569) 2026-05-06 05:40:33 +08:00
Junmo KimandGitHub 136badb86e fix(gemini): surface tool_call input on Gemini ACP cards (#562)
* fix(acp): derive tool_call input from kind+title fallback

Gemini 2.5 Flash and 3 Flash Preview omit rawInput entirely on
tool_call events while emitting prose (non-JSON) thoughts. Neither
the existing rawInput path nor JSON-thought hoisting fires, so the
UI shows "Input: null" alongside a perfectly readable title like
"README.md" or "ls -la /tmp".

Add a conservative fallback that maps known kinds to a minimal
input shape:

  read     -> { file_path: title }
  execute  -> { command: title }
  search   -> { pattern: title }
  think    -> null  (topic-update prose has no clean arg mapping)
  unknown  -> null  (no guessing on shapes we have not verified)

Priority: rawInput > hoisted JSON thought > kind+title derive.

Lock the new behaviour with synthetic unit tests (8 cases) and a
real-Gemini fixture suite captured from gemini-3-flash-preview
and gemini-2.5-flash via ACP stdio (4 fixtures, 33/27/13/4 raw
sessionUpdate events). The fixtures double as regression guards
against future ACP handler changes.

* fix(web): suppress duplicate subtitle when equal to tool title

Gemini ACP emits a tool_call whose title field is a human-readable
summary (often the verbatim shell command or file path). Combined with
the kind+title input fallback, an unknown-tool card ends up with the
same string in both the title and subtitle slots — e.g. title
"cat /tmp/hello.txt" over subtitle "cat /tmp/hello.txt".

Add a guard in getToolPresentation's unknown-tool branch: emit
subtitle only when it differs from toolName. The known-tool and
mcp__* branches are unaffected.

* test(acp): align Gemini fixtures to current model set

- Drop gemini-2.5-flash fixtures: the captures came from a model that
  is not part of the PR's evidence model set, and re-running the
  capture is gated on quota that is not currently available.
- Refresh gemini-3-flash-preview read_file / run_shell fixtures with
  a fresh live capture so they reflect the latest ACP shape (e.g.
  a `kind: think` tool_call expressing reasoning when the model emits
  no agent_thought_chunk).
- Update fixture-replay expectations: read_file no longer requires
  reasoning chunks (zero are emitted on this path) and now requires
  >= 2 tool_calls (think + read).

* feat(web): promote semantic title for Gemini ACP tool cards

When the unknown-tool ToolCard would render the same string as both
the title and the subtitle, promote a semantic label to the title
slot so the card reads like a sentence:

  cat /tmp/hello.txt   →   Run shell  / cat /tmp/hello.txt
  README.md            →   Read file  / README.md
  *.ts                 →   Search     / *.ts

This is a web-only ergonomic change; the underlying ACP message
shape (tool_name = title, input = derived from kind+title) is
unchanged. Builds on the dedup guard so the title-equals-subtitle
case is now handled by promotion rather than by hiding the subtitle.

* fix(acp): derive tool_call.input for kind=edit from locations[0].path

Gemini's write_file and replace tools both surface as ACP tool_call
with kind="edit" and rawInput omitted. The path lives on locations[0]
from the very first event; the title is prose like "Writing to foo.txt"
or "foo.txt: old => new", which is not safely usable as a file_path.

Extend the kind+title fallback to read locations[0].path when kind is
"edit", and synthesize { file_path } from it. Title fallback is
intentionally not used here so we never feed prose into file_path.

Lock the behaviour in with two new fixtures captured live from
gemini-3-flash-preview (write_file and replace) plus two synthetic
unit tests covering the locations-present and locations-empty paths.

* test(acp): add gemini-3.1-pro-preview fixtures for regression coverage

Captured 4 raw ACP `sessionUpdate` sequences from a live
`gemini-3.1-pro-preview` session via the same isolated hub +
runner + spawn pattern used for the existing flash captures
(read_file 31 events / run_shell 83 events / write_file 4 events /
edit_file 11 events).

The pro tier reuses the same kind/title shape as flash:
`rawInput` is omitted on every tool_call across read / execute /
edit kinds, so the kind+title (and locations[0].path for edit)
fallback is exactly what derives the modal Input. Locking these
fixtures in guards against future regressions on a second model.

The fixture-based regression test gains 4 entries (read / shell /
write / edit) mirroring the flash matrix; assertions are unchanged.
ACP handler suite: 53 -> 57 pass.
2026-05-05 18:20:54 +08:00
Junmo KimandGitHub d9d7ed6699 feat(web): show message metadata (invoke time, duration, model) on click (#555)
* feat(web): show message metadata (invoke time, duration, model) on click

* fix(cli): preserve model field on assistant messages forwarded to hub

`RawMessageSchema` validates the `message` object in Claude Code session
JSONL lines before the cli forwards each message to the hub. Zod's default
parse mode strips fields that the schema does not declare, so the
`message.model` value (e.g. `claude-sonnet-4-6`) was silently removed
before the message reached the hub. The web normalizer reads
`data.message.model` to label assistant blocks, so without this field
every assistant message fell back to a generic "AI Model" label —
defeating the per-message model attribution this PR adds.

Add `model` to `RawMessageSchema` so it survives parse and reaches the
hub intact.

* fix(web): drop dead model shorthand in result envelope normalize

The `result/success` branch in `normalizeAgentRecord` referenced a `model`
identifier that was never declared in the function scope, breaking
`bun typecheck`. The reducer that consumes the resulting `turn-duration`
event does not look at `model` on the event itself, so the shorthand was
dead code. Remove it to restore typecheck.

* refactor(web): simplify turn-duration matcher with findLastIndex

Replace the imperative reverse-scan loops in the `turn-duration` reducer
branch with `findLastIndex`. The previous fallback also had an awkward
double-loop that mutated the matched block in place; using an index plus
a single immutable update keeps the block reference clean and makes the
match priority (id-prefix > tool-call id > last assistant-like) explicit.

Behaviour is unchanged — existing reducer tests cover both the messageId
match and the fallback paths.

* fix(web): preserve per-message model across mid-session model switches

The metadata footer fell back to `Session.model` from chat context when a
message did not carry its own `model`. That session value mutates when
the user switches models mid-session, so older messages were relabeled
with the latest model — including Codex/local assistant paths
(`AGENT_MESSAGE_PAYLOAD_TYPE`) that don't populate `msg.model`.

Drop the mutable-context fallback: pass `messageModel ?? null` to
`MessageMetadata` and let it omit the model line when no per-message
value is available. This is correct behaviour for messages whose
producer didn't record a model, and avoids ever attributing a message
to a model that didn't generate it.

Also remove the now-unused `useHappyChatContext` import in this file.

Add reducer invariants to lock in the data flow:
- `preserves per-message model across mid-session model switches`
- `leaves model undefined when message lacks per-message model`

* fix(web): keep tool-block reference identity when applying turn-duration

`ensureToolBlock` stores the same `ToolCallBlock` instance in both
`toolBlocksById` and `blocks`. The earlier refactor cloned the matched
block via `blocks[foundIndex] = { ...b, durationMs }`, which left the
map pointing at the stale original. A subsequent permission/result
mutation through `ensureToolBlock` would then update the stale map
object while the rendered `blocks` entry never sees the completion or
result, causing tool cards to miss state transitions.

Mutate the matched block in place instead — same in-place pattern the
reducer used before — and gate the assignment on the kinds that carry a
`durationMs` field so TypeScript narrows correctly.

Add an invariant test that fires a `turn-duration` event at a tool-call
block and asserts the rendered block and `toolBlocksById.get(...)`
remain the same object reference.

* fix(web): do not render service_tier as the model id

`MessageMetadata` previously fell back to `usage.service_tier` as the
"model" when no per-message `model` was available, so messages without
their own model id could surface labels like `Model: standard_only` —
service_tier is tier metadata, not a model.

Render the model line only when a real `model` is present; if a
non-`standard` `service_tier` is the only signal, surface it as a
separate `Tier: <tier>` label so it is not mistaken for the model.
The standard tier is the implicit default and is never rendered alone.

Extract the label-building logic into `buildMessageMetadataLabels` so
it can be unit-tested without a DOM. Add tests covering: model present,
model missing with non-standard tier, default standard tier, model with
non-standard tier appended, and the empty-input case.

* fix(web): metadata toggle ignores clicks on nested interactive controls

The bubble-level click handler that opens the metadata footer wraps
interactive descendants — tool-card buttons, retry buttons, dialog
triggers (Radix `role="button"`), and the Markdown code-copy button.
Clicking any of those flips the metadata footer as a side effect, even
when the descendant is the actual target of the user's intent.

Extract the closest-ancestor check into a small `metadataToggle` helper
and route both `AssistantMessage` and `UserMessage` click paths through
it. The toggle bails out when the click target sits inside any
`button`, `a`, `input`, `textarea`, `select`, or `[role="button"]`
ancestor; plain message-body text still toggles as before.

Add unit tests covering: button target, nested span inside a button,
`role="button"` Radix-style trigger, anchor/input/textarea/select form
controls, plain message-body text (no toggle), and a non-HTMLElement
target.

* fix(cli): preserve messageId on system/turn_duration record

`web/src/chat/normalizeAgent.ts` matches each `turn-duration` event to
the assistant block carrying the same `data.messageId`. Claude code
emits that field on the `system/turn_duration` record, but
`RawJSONLinesSchema`'s system branch did not declare `messageId`, so
Zod stripped it before the cli forwarded the record to the hub. The
matcher then fell back to "the last visible block", which is wrong for
interleaved/tool-heavy turns and silently attaches the duration to the
wrong assistant block.

Add `messageId: z.string().optional()` to the system schema so the id
survives parse and reaches the web reducer. Tests cover the preserved
case, the legacy case without `messageId`, and the previously-fixed
`message.model` case so Zod strip regressions on adjacent fields stay
locked in.

* fix(web): metadata toggle accepts SVG event targets

`isClickOnNestedControl` only walked up via `closest` when the click
target was an `HTMLElement`. The copy / retry / Markdown code-copy
buttons render SVG icons, so clicking the icon makes the event target
an `SVGElement` (not an `HTMLElement`) — the guard returned false and
the bubble-level click flipped the metadata footer anyway.

Widen the type check to `Element`, which is the common super-class of
both `HTMLElement` and `SVGElement` and also exposes `closest`. Plain
text targets and non-Element targets still behave as before.

Add a regression test that mounts an icon-only button (`<button><svg>
<path/></svg></button>`) and asserts both the `<svg>` and `<path>`
targets walk up to the enclosing button.

* refactor(cli): rely on Zod passthrough for jsonl envelopes

`RawMessageSchema` and the `system` branch of `RawJSONLinesSchema` were
declared with Zod's default `strip` mode, so any field the cli did not
explicitly enumerate was silently dropped before the hub forwarded the
record. The metadata pipeline lost `message.model` and
`system/turn_duration.messageId` exactly that way, and each gap took a
separate fix.

Switch both schemas to `.passthrough()` so undeclared fields survive
parse and reach the web reducer verbatim. Future SDK additions no
longer require another schema patch.

Add tests asserting that unknown keys on assistant messages and
unknown keys on system records (alongside the existing `messageId`
case) are preserved end-to-end through the schema.

* refactor(web): clean up dead metadata propagation surface

Several knobs were added to thread metadata through the chat tree but
ended up unused or redundant; consolidate them so the data flow has a
single canonical path.

- Drop the unreachable `data.type === 'result' && data.subtype ===
  'success'` branch in `normalizeAgentRecord`. Claude's `result`
  records are consumed by `claudeRemote` as session-completion signals
  and never forwarded to the hub; the cli `RawJSONLinesSchema`
  discriminator does not include `result`, so these records are
  rejected before they reach `normalizeAgentRecord` either way.
- Stop threading `invokedAt` through the inner `normalizeAssistantOutput`
  / `normalizeUserOutput` / `normalizeAgentRecord` calls. Every caller
  in `normalizeDecryptedMessage` already overwrites it via the outer
  spread, so the inner copies were dead writes. Set `invokedAt` only at
  the outer boundary.
- Remove the `model?: string | null` field from `HappyChatContextValue`
  and the `model` prop on `HappyThread` / `SessionChat`. Its only
  consumer (`AssistantMessage` mutable-fallback) was removed when the
  per-message model attribution fix landed; the prop has no readers
  now.
- Match the existing `as Partial<HappyChatMessageMetadata> | undefined`
  cast pattern in `AssistantMessage` and `UserMessage` instead of the
  non-`Partial` cast that pretended every field was present even when
  `custom` is undefined.
- Rename `AgentEvent.turn-duration.messageId` to `targetMessageId` so a
  reader does not confuse the duration's target with the surrounding
  envelope id; the wire field on Claude's `system/turn_duration` record
  stays `messageId` (vendor name) and is mapped at the normalize
  boundary.

No behaviour change. All existing tests pass.

* fix(web): turn-duration matcher and cli-output merge precedence

Two reducer-level metadata-correctness bugs surfaced during a hostile
self-review.

1. Turn-duration matcher silently dropped the duration when
   `targetMessageId` resolved to a non-duration-bearing block. The
   existing pipeline did `findLastIndex(b => b.id === targetId || ...)`
   first; if that hit an `agent-event` or `user-text` block (id-prefix
   collision), the kind guard at the assignment site failed and the
   duration was never attached. The fallback search ran only when the
   first pass returned -1, not when the kind check rejected the match.

   Fold the kind filter into every search predicate via a typed
   `isDurationTarget` helper so the priority `target-bearing match >
   tool-call id > last duration-bearing block` is exhaustive.

2. `mergeCliOutputBlocks` had asymmetric metadata precedence between
   the command-name block (`prev`) and the stdout follow-up (`block`):
   `invokedAt` and `model` preferred prev, but `durationMs` and `usage`
   preferred block. Only the command-name block carries first-class
   metadata; the stdout follow-up is a synthetic split. Use prev as the
   primary source uniformly and fall back to block only when prev is
   missing the field.

Tests cover the fallback path on the matcher and both precedence
scenarios on the merger.

* fix(web): preserve tool-call invokedAt across tool-result update

`ensureToolBlock` is called twice for the same tool: first with the
seed from the assistant's tool-use block, then with the seed from the
matching tool-result message. The second call's `seed.invokedAt` came
from the tool-result message and was unconditionally overwriting the
tool-call's original invokedAt. The rendered "Invoke" timestamp on a
tool card therefore showed when the result was processed, contradicting
the column header.

Guard the assignment so the timestamp survives the second call —
`existing.invokedAt ??= seed.invokedAt` semantics — while still letting
the first call set the value when the tool is created. `durationMs`,
`usage`, and `model` continue to overwrite because their values come
from the result message's usage block and are intentionally newer.

Add a regression test that fires a tool-use followed by a tool-result
with a later invokedAt and asserts the tool block keeps the original.

* fix(web): metadata footer UX, accessibility, and label hardening

Bundle the remaining UI surface fixes for the metadata footer.

- Make the bubble interactive only when there is metadata to disclose.
  Without the guard, every non-Claude session bubble (Codex / Cursor /
  Gemini, none of which populate `model`/`usage`/`durationMs` in the web
  layer) showed a pointer cursor and reacted to clicks even though
  `MessageMetadata` rendered nothing — false-positive interactivity.
- Add keyboard support: when the bubble is interactive it now exposes
  `role="button"`, `tabIndex=0`, `aria-expanded`, and an `Enter`/`Space`
  key handler so screen readers and keyboard-only users can disclose
  the footer the same way mouse users do.
- Fix nullish-vs-falsy bugs in the label builder: a 0 ms turn or a 0
  unix-epoch invokedAt no longer hides their lines. Use explicit
  `!= null` / `>= 0` checks.
- Rename the token total to "billable tokens" so the explicit exclusion
  of cache I/O is signalled in the label rather than implied by the
  number alone.
- Tag the queued/sending status spans with `role="status"` (and an
  accessible label) so they are announced by AT and so the metadata
  toggle's `closest('button, ..., [role="status"]')` filter does not
  accidentally fire when a user clicks a status icon.
- Add the native `<summary>` element and `[role="status"]` to the
  toggle's nested-control selector. Tool cards already render their
  expandable bodies as `<details><summary>` — clicking the summary now
  expands the disclosure without also flipping the metadata footer.

Tests cover: native `<summary>` target, `role="status"` target, the
billable label, durationMs=0 surfaced, invokedAt=0 surfaced,
invokedAt=null/undefined hidden.

* fix(web): expose cli-output metadata via dedicated toggle button

CliOutputBlock renders the entire card as a Dialog trigger <button>, so
the bubble-level click handler on the cli-output branch never opened the
metadata footer by mouse — every click landed inside that button and
isClickOnNestedControl bailed out. The wrapping div with role="button"
was also a nested-interactive a11y anti-pattern.

Drop the wrapper's role/onClick/tabIndex/keyDown on the cli-output
branch and render an explicit "Show metadata" / "Hide metadata" button
beneath the card. The dialog trigger keeps its full hit area; the
metadata footer is now reachable by both mouse and keyboard.

* fix(web): exclude toggle wrapper from nested-control guard

The bubble-level toggle wrappers in AssistantMessage / UserMessage
carry role="button" for keyboard accessibility. Without excluding
currentTarget, closest('[role="button"]') from any inner click matches
the wrapper itself and the toggle bails out — making the metadata
footer unreachable for mouse users (keyboard Enter/Space still worked,
which is why unit tests and bot review missed it).

Walk currentTarget out of the match: a nested control is one whose
closest matching ancestor is *not* the wrapper itself.

* fix(web): apply nested-control guard on keyboard activation too

The mouse path bailed via isClickOnNestedControl, but the keyboard path
on the metadata-toggle wrapper did not. Pressing Enter or Space on a
focused descendant control (e.g. Markdown code-copy button) bubbled the
keydown up and the wrapper toggled metadata alongside the descendant's
own activation.

Generalize the helper to isNestedInteractiveEvent over both
MouseEvent and KeyboardEvent and call it from onMetadataKeyDown in
AssistantMessage and UserMessage.
2026-05-03 12:51:22 +08:00
Junmo KimandGitHub 9ee014098a feat(opencode): support model selection and mid-session model change (#558)
* refactor(opencode): declare ModelChange capability and add model field to OpencodeMode

Mark opencode flavor as supporting model change by adding Capabilities.ModelChange
to FLAVOR_CAPS.opencode. Add optional `model` field to OpencodeMode so the
set-session-config handler can carry a model alongside the existing permissionMode.

Pure structural change: no behavior change yet. The mid-session model change RPC
and UI wiring follow in subsequent commits, gated by this capability.

* feat(acp): branch setModel by flavor, capture session models metadata, expose getSessionModelsMetadata on AgentBackend interface

Adds an optional `flavor` argument to `AcpSdkBackend.setModel` so it can
dispatch the right `session/*` RPC for each agent flavor without changing
the call site Gemini already uses. Both Gemini and OpenCode wire to
`session/set_model`; the OpenCode response only carries `_meta.opencode`,
so the backend updates the cached `currentModelId` optimistically while
preserving the previously captured `availableModels`.

Captures `availableModels` and `currentModelId` from `session/new` /
`session/load` / `session/set_model` responses into per-session metadata,
exposed as `getSessionModelsMetadata(sessionId)` on the `AgentBackend`
interface so the hub can forward the snapshot to the web client.

* feat(opencode): accept model in set-session-config RPC and forward to launcher

Mirror the Gemini set-session-config handler so the web UI can change the
OpenCode model mid-session. Validates incoming model strings, persists null
("Default") for keepalive metadata, and pushes a keepAlive immediately so
the hub UI reflects the change without waiting for the next 2s tick.

Forward the model through opencodeLoop and into queued OpencodeMode entries
so the launcher can detect a per-batch model change. Add a setModel helper
on OpencodeSession to store the chosen model on the shared session base.

Wire --model up the runner path: parse --model <value> in commands/opencode.ts
and stop excluding opencode in buildCliArgs so the runner spawns OpenCode
with the user-selected initial model.

* feat(opencode): switch model mid-session via ACP RPC

Mirror the Gemini pattern from PR #543: when a user picks a different model
between turns, call backend.setModel with flavor='opencode' so the ACP backend
sends session/set_session_config_option (configId='model') to the running
OpenCode CLI. The next turn then runs against the new model.

The first batch on a fresh session seeds currentBackendModel without firing the
RPC — the OpenCode CLI was launched with that model via --model and there is
nothing to switch yet. If the running build does not implement the RPC we learn
that from the first method-not-found response, latch inline switching off, and
notify the user once. Other errors fall back to the previous model and surface
a one-line failure message.

* feat(hub): expose model selection and discovery for OpenCode sessions

Generalize the /sessions/:id/model guard via supportsModelChange so any flavor
that advertises the ModelChange capability becomes accepted automatically. This
piggybacks on the capability SSOT introduced in PR #400 and turns OpenCode on
without listing flavors inline.

Add a /sessions/:id/opencode-models endpoint that mirrors the existing
codex-models pattern. The endpoint forwards a per-session listOpencodeModels
RPC to the running OpenCode launcher, which returns the availableModels and
currentModelId metadata captured from the ACP session/new and
session/set_session_config_option responses. The web UI consumes this to
render the model dropdown without round-tripping ACP itself.

* feat(web): render OpenCode model dropdown in the chat composer

Mirror the Codex pattern in SessionChat: query /sessions/:id/opencode-models
via a new useOpencodeModels hook and feed the result into the composer's
availableModelOptions. The AssistantChat model dropdown now lists the user's
ollama / mlx / OpenCode Zen models with the same provider/model label that the
ACP server reports, so the picker matches the OpenCode TUI.

Stop falling back to the Claude composer model list when the flavor is
opencode and no custom options are supplied — that fallback briefly surfaced
unrelated Claude models in OpenCode sessions before the RPC response landed.
The NewSession flow keeps an empty MODEL_OPTIONS.opencode for now: model
discovery requires an active OpenCode ACP session, so the dropdown becomes
available once the session boots and stays empty (and hidden) at creation
time.

* feat(cli,hub): add cwd-based OpenCode model discovery RPC

Adds a short-lived `opencode acp` probe that runs `initialize` +
`session/new` against a target cwd to read the `availableModels` /
`currentModelId` snapshot, then tears the subprocess down. Results are
cached for 60s per cwd and concurrent probes coalesce into a single
spawn.

Exposes the probe through:
- `listOpencodeModelsForCwd` JSON-RPC handler on the CLI
- `RpcGateway.listOpencodeModelsForCwd` / `SyncEngine.listOpencodeModelsForCwd`
- `GET /api/machines/:id/opencode-models?cwd=...` on the hub

This lets the web NewSession form discover OpenCode models for a chosen
directory before any session is spawned.

* feat(web): add OpenCode model selector to NewSession with loading and default highlight

Adds a `OpencodeModelSelector` panel that the NewSession form swaps in
when the OpenCode flavor is selected. The panel:

- queries the new `GET /api/machines/:id/opencode-models?cwd=...` endpoint
  via `useOpencodeModelsForCwd` (TanStack Query, 60s staleTime, no retry),
- shows a labelled spinner + skeleton rows while discovering,
- renders an inline error with a Retry button when probing fails,
- renders an empty-state message when the directory yields no models,
- highlights the OpenCode-reported `currentModelId` with a "Default" badge
  and auto-selects it (or the first option) so the form has a sensible
  value if the user hits Enter without scrolling.

Selection is reset whenever the agent / machine / directory changes so a
new probe can establish a fresh default. Directory input is wrapped in
`useDeferredValue` so per-keystroke edits do not spawn a fresh
`opencode acp` probe. The chosen model is forwarded on session spawn
via the existing OpenCode `model` parameter.

Adds en + zh-CN locale strings for the loading / failure / empty / retry
/ default-badge labels.

* fix(cli): guard /machines/:id/opencode-models handler with workspace root check

The machine-scoped `listOpencodeModelsForCwd` RPC handler was registered by
`registerCommonHandlers` without any workspace-root check, so a web client
could pass an arbitrary `cwd` and have the runner spawn an `opencode acp`
subprocess plus a `session/new` against that path. That broke runner
isolation, since peer machine-scoped handlers (`list-directory`,
`spawn-happy-session`) already enforce the configured workspace root.

Re-register the handler in `ApiMachineClient` so it reuses the existing
`resolveForWorkspaceCheck` (realpath-based, with missing-tail walking) and
`isWithinWorkspaceRoot` helpers before delegating to the lower-level probe.
The resolved cwd is forwarded down so symlinked-but-contained paths still
work, while traversal attempts are rejected with the same error shape the
peer handlers use. Added unit tests around the new dispatch path. Addresses
HAPI Bot review on PR #558.

* fix(web): gate opencode model discovery on cwd existence

The new-session form previously enabled `useOpencodeModelsForCwd` as
soon as the OpenCode agent, machine, and any non-empty directory string
were present. Because that hook calls `/machines/:id/opencode-models`
and the CLI handler starts an `opencode acp` probe for that cwd, normal
typing through partial paths could launch expensive 30s OpenCode
subprocesses for non-existent directories before the path-existence
result had validated the final cwd.

Reorder NewSession so `useMachinePathsExists` runs before
`useOpencodeModelsForCwd`, then gate the discovery hook on the
directory having been positively confirmed to exist
(`pathExistence[deferredDirectory] === true`). The decision is
factored into a small pure helper `shouldEnableOpencodeModelDiscovery`
so the contract can be unit-tested without provider scaffolding.

* fix(web): keep current opencode model on shortcut without dynamic options

`getNextModelForFlavor` is invoked by the global Ctrl/Cmd+M shortcut in
`HappyComposer`, which is now active for OpenCode sessions because the
agent backend declares the `ModelChange` capability. When the dynamic
OpenCode model list has not yet been loaded — e.g. the user presses the
shortcut before `/opencode-models` returns — the function received an
`undefined`/empty `customOptions` and fell through to the Claude preset
cycler, which would emit `sonnet`/`opus` for an OpenCode session. The
following turn then attempted `session/set_model` with a Claude model id
that no OpenCode provider can serve.

Add an `opencode` branch that returns the (normalized) current model
unchanged when no dynamic options are available, mirroring the existing
empty-list policy of `getModelOptionsForFlavor`. Unit tests cover the
undefined / empty / null-current-model variants and lock the
no-Claude-fallback contract. Addresses HAPI Bot review on PR #558.
2026-05-03 12:50:22 +08:00
CoColateandGitHub e76738aa5a fix(codex): improve web rendering for Codex events (#544)
* test(codex): add web event rendering harness

* fix(codex): surface plan updates in web

* fix(codex): render MCP tool calls in web

* fix(codex): improve terminal and context display

* fix(codex): format token usage events

* fix(codex): show status context in web

* fix(codex): preserve tool result errors
2026-04-29 17:22:45 +08:00
NightWatcher314andGitHub a612be50d6 Add Codex clear and compact slash commands (#541)
* Add Codex clear and compact slash commands

* Stabilize queued thinking event test

* Interrupt active Codex turn before slash commands
2026-04-29 14:15:44 +08:00
Junmo KimandGitHub 0160da4bb5 fix(gemini): switch model mid-session via ACP RPC (#543)
When a user selects a different Gemini model from the Web UI mid-session,
the running `gemini --experimental-acp` process kept using the model it
was launched with. The Web UI reflected the new selection, but the next
response was still produced by the original model.

Changes:
- AcpSdkBackend: add `setModel` wrapping the `session/set_model` RPC.
  Errors propagate as standard rejections, matching every other
  `sendRequest` call in this file.
- geminiRemoteLauncher: detect model changes between turns and call
  `backend.setModel` on the live ACP session — no process restart, no
  MCP reload. If the running gemini-cli build returns method-not-found,
  the launcher learns once, surfaces a single advisory message, then
  silently honors the previous model for the rest of the session.
- AgentSessionBase.pushKeepAlive: small helper used by runGemini to
  broadcast new config to the hub immediately after `set-session-config`.
- Both layers serialize the switch — the launcher attempts `setModel`
  only between batches, and `AcpSdkBackend.setModel` defensively awaits
  `waitForResponseComplete()` before issuing the RPC.

Tests:
- New `geminiRemoteLauncher.test.ts` covers: setModel called between
  turns when the model differs; not called when unchanged; the
  method-not-found capability latch; transient errors continue with the
  previous model; setModel is serialized after the prior prompt.
- `runGemini.test.ts` asserts pushKeepAlive fires from the
  `set-session-config` handler.
2026-04-29 09:22:50 +08:00
CoColateandGitHub 9d2dec137b feat(codex): support slash controls and skill discovery (#545)
* feat(codex): resolve slash controls before sending to Codex

* feat(codex): discover commands and skills

* fix(codex): handle slash commands before attachments

* fix(codex): block remaining unsupported built-ins
2026-04-29 09:20:11 +08:00
weishu c9ff7f9de3 Release version 0.17.2 2026-04-27 21:16:18 +08:00
weishu cfc1edb747 fix(codex): subagent session hook may override primary session 2026-04-27 21:13:06 +08:00
weishu 0cfc3b4ed2 fix(cli): preserve codex config args on Windows 2026-04-26 12:05:44 +08:00
010dc41369 feat: workspace browser with --workspace-root opt-in scoping (#526)
* feat(web): add workspace browser for multi-directory navigation

Add /browse route with a folder browser that lets users navigate
filesystem directories on connected machines and launch sessions
from any folder. Supports saved workspace paths and direct path
input. The "Start Session" action pre-fills the NewSession form.

- CLI: register machine-level `list-directory` RPC handler
- Hub: add POST /machines/:id/list-directory route
- Web: add WorkspaceBrowser component with git repo detection
- Web: add /browse route with navigation from sessions sidebar
- Web: support initialDirectory/initialMachineId in NewSession

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* feat: add --workspace-root opt-in scoping for /browse and session spawn

Adds a single new flag, \`--workspace-root <path>\` (with \`~\` / \`~/foo\`
expansion), on \`hapi runner start\` and \`hapi runner start-sync\`.

When set:
- The runner reports the path in machine metadata.
- The list-directory and spawn-session RPC handlers reject paths outside
  the root, so the web UI can't escape the configured tree even if
  someone crafts a request manually.
- The /browse page in the web UI auto-opens that root, restricts the
  breadcrumb / go-up to its subtree, and shows directory entries with
  git-repo annotations.
- The /sessions/new form keeps its existing free-text directory input
  plus autocomplete + recent-paths chips, and gains a small "Browse"
  button (next to the input) that opens /browse for picking a folder.
- Reconnect-time metadata sync ensures stale records get the field
  filled in (or cleared when the flag is dropped on a later restart),
  so the hub state matches the CLI's intent.

When unset:
- Runner behaves like the legacy hapi (no scoping, no browse feature).
- /browse renders an informative state pointing at the flag instead of
  blocking the user.
- The /sessions/new form looks identical to the pre-change behavior;
  the "Browse" button is hidden.

Includes a startup banner so \`runner start-sync\` no longer looks like
it hung, and surfaces the workspace-root sync result on stdout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(hub): preserve workspaceRoot when rehydrating machines from store

MachineCache.refreshMachine() rebuilt the metadata object from an
explicit field allowlist, so any field not in the list (including the
new workspaceRoot) was silently dropped on every read — even though it
was correctly written to the store.

Add workspaceRoot to the zod schema, the Machine interface, and the
hand-rolled projection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(web): friendlier empty state on /sessions

When there are zero sessions the page used to be a vast blank
rectangle with just the "0 sessions in 0 projects" caption. Render a
centered empty state instead: a calendar/agenda icon, a short heading
and hint, and two buttons — "Start a session" (→ /sessions/new) and
"Browse workspace" (→ /browse).

SessionList gains an optional onBrowse prop. Router wires it on the
sessions page so the secondary button resolves; other callers can leave
it unset to hide that button.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: document --workspace-root flag in cli/README and root README

Add a short paragraph under "Runner management" in cli/README.md
explaining what \`--workspace-root\` enables (scoped /browse tree,
list/spawn enforcement, tilde expansion) and that omitting it keeps
the legacy behavior. Mention the workspace browser in the top-level
README's Features list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: address PR #526 review feedback

Three findings from the review bot:

1. [Major] Workspace-scope check was lexical only. With workspaceRoot
   = /safe, a symlink such as /safe/out -> /etc would pass the relative-
   path test and let list-directory / spawn-happy-session reach paths
   outside the configured root. realpath the workspaceRoot at construction
   time, and resolve every incoming path through realpath (walking up to
   the nearest existing parent for spawn targets that haven't been
   created yet) before the containment check.

2. [Minor] \`hapi runner start --workspace-root\` with no value used to
   drop the flag silently and start the runner unscoped. Now treats a
   missing or flag-shaped next argument as an error.

3. [Minor] /sessions/new's "Browse" button always opened /browse using
   localStorage's last-used machine, ignoring the user's current
   selection. NewSession already passes machineId in its callback;
   forward it through the /browse search params and seed
   WorkspaceBrowser with it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): gate list-directory RPC behind --workspace-root opt-in

Without a configured workspaceRoot, isWithinWorkspaceRoot() returns
true unconditionally, leaving the new list-directory RPC able to
enumerate any path on the runner. The Web UI already hides Browse
for these machines, but the backend should enforce the opt-in too.

Refuse the RPC up front when no workspace root is configured.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-26 09:59:18 +08:00
xiaobaifly7andGitHub 4ec9537e4a feat(hub): add ServerChan task notifications (#515)
* fix(hub): 修复发送后状态显示延迟

* feat(hub): 接入Server酱任务通知

* fix(hub): 仅在会话结束时发送完成通知

* fix(hub): avoid reviving inactive queued sessions

* fix(hub): address notification review feedback

* fix(hub): expire queued thinking on hub clock

* fix(hub): 修复任务通知 review 反馈
2026-04-25 22:01:02 +08:00
8118408796 fix(runner): prevent ghost sessions from orphaned spawn webhooks (#440)
* fix(runner): prevent ghost sessions from orphaned spawn webhooks

spawnSession() registers a tracked session entry before the child's
"Session started" webhook arrives. When the 15s webhook timeout fires,
only pidToAwaiter / pidToErrorAwaiter are cleared; the
pidToTrackedSession entry is left in place and the detached child
keeps running. If the child eventually starts and reports its webhook,
onHappySessionWebhook() still finds the stale tracking entry and
promotes the orphan into a normal runner-managed session, surfacing
a message-less "ghost session" in the web UI.

This is easy to reproduce on opus[1m] --resume: observed real-world
case where four rapid "resume" clicks produced four orphan children
that all reported their webhooks ~60 minutes later, creating four
ghost sessions on the dashboard.

Three-part fix:

1. Make the webhook timeout configurable via
   HAPI_RUNNER_WEBHOOK_TIMEOUT_MS (default unchanged at 15_000). Users
   on slow models / large resumes can raise the ceiling so the
   timeout never fires in the first place.

2. On timeout, also delete the pidToTrackedSession entry and SIGTERM
   the child, so a late webhook cannot promote the orphan.

3. Defence in depth: if onHappySessionWebhook() receives a webhook
   from a PID that is not tracked but whose payload claims
   startedBy: 'runner', ignore it and SIGTERM the child. Genuine
   terminal-launched children correctly report
   startedBy: 'terminal', so this branch cannot false-positive on
   them.

* fix(runner): use tree-kill on timeout and clean up worktree for orphans

Address review feedback on the kill path and worktree cleanup:

1. Timeout handler: replace bare `happyProcess.kill('SIGTERM')` with
   `killProcessByChildProcess(happyProcess)` so the entire process tree
   (wrapper + detached agent grandchildren) is reaped, matching the
   existing `stopSession()` behaviour.

2. Orphan webhook handler: replace bare `process.kill(pid, 'SIGTERM')`
   with `killProcess(pid)` for proper SIGTERM → SIGKILL escalation.
   A ChildProcess reference is unavailable here (tracking entry already
   removed), so tree-kill is not possible — but the timeout handler
   should have already tree-killed the group; this is defence-in-depth.

3. Worktree leak: when a worktree session times out, register a
   one-shot `exit` listener on the child process to run
   `cleanupWorktree()` after the child actually exits.  Previously
   `maybeCleanupWorktree('spawn-error')` would skip cleanup because
   the child was still alive at that point, and `onChildExited()` had
   no worktree awareness after the tracking entry was deleted — so the
   worktree leaked permanently.

---------

Co-authored-by: fengtian <fengtian@users.noreply.github.com>
2026-04-25 10:55:59 +08:00
weishu be47ad06b9 Release version 0.17.1 2026-04-25 10:54:49 +08:00
weishu 97be34e21c Add Codex model selection 2026-04-25 10:48:12 +08:00
weishu 11e1d50e46 remove transcript timeout for codex 2026-04-24 13:45:16 +08:00
weishu 83e5053156 Release version 0.17.0 2026-04-24 12:07:52 +08:00
weishu d87b6c6996 guard local codex startup by version 2026-04-24 11:17:39 +08:00
weishu 8e54bbeb53 feat(codex): use hook to obtain the session id and transcript path 2026-04-24 10:52:38 +08:00
Junmo KimandGitHub 82703b85fb fix(acp): normalize tool_call_update content for agents without rawOutput (#521) 2026-04-24 08:13:34 +08:00
weishu 938c897ed9 Release version 0.16.8 2026-04-23 20:52:44 +08:00
19e74456fb fix(codex): surface thread system errors (#519)
Co-authored-by: Liu-KM <Liu-KM@users.noreply.github.com>
2026-04-23 20:45:33 +08:00
Junmo KimandGitHub 96d766d7f1 feat(acp): forward agent_thought_chunk as reasoning message (#520)
* refactor(agent): extend AgentMessage and CodexMessage unions with reasoning variant

Add a reasoning variant to the shared AgentMessage union that flows
out of the ACP backend, and pass it through to CodexMessage so the
existing web reducer (which already renders { type: 'reasoning' }
parts as collapsible blocks) can consume ACP-sourced thoughts
identically to Codex.

No behavior change yet: the ACP handler still drops thought chunks,
and the remote launchers receive the new variant as a no-op. The
behavior is wired up in the following commit.

* feat(acp): forward agent_thought_chunk as reasoning message

Route ACP thought chunks to the session as reasoning AgentMessages so
OpenCode and Gemini thinking output reaches the web UI's Reasoning
block, matching the existing Codex behavior.

Thought chunks are emitted inline without flushing the pending text
buffer — text and thought live on independent interleave lanes, so
splitting a live text segment on every thought arrival would be
wrong. The inline-emit ordering is documented alongside the test
that depends on it.

extractTextContent is not reused for thought content: its
assistant-audience filter is correct for regular message chunks but
would silently drop thoughts annotated with a non-assistant audience,
which have no meaningful audience to filter against. A direct text
block shape check handles the narrower need.

In the remote launchers, reasoning is surfaced to the local terminal
buffer as a truncated system-role hint prefixed with [Thinking],
matching how the Codex flavor already displays reasoning chunks
in-terminal without mixing them into the assistant reply stream.
2026-04-23 20:45:20 +08:00
Junmo KimandGitHub 3405b56ff2 fix(cli): preserve intra-turn order between text and tool updates in ACP (#505) 2026-04-21 21:10:08 +08:00
Junmo KimandGitHub 32755f9056 feat(web): show queued status for messages pending inference (#492) 2026-04-20 19:49:26 +08:00
hu chenxiandGitHub cccee4da63 fix(cli): set TERM, COLORTERM, and LANG defaults for terminal pty sessions (#495) 2026-04-20 19:47:55 +08:00
weishu 5bb27ab8ba Release version 0.16.7 2026-04-18 10:48:21 +08:00
b9d28f4d30 fix(cli): include ~/.codex/skills in user skill discovery (#482) (#487)
The skill listing only scanned ~/.agents/skills and ~/.claude/skills
for user-level skills, ignoring ~/.codex/skills where Codex users
commonly store their skills. Add ~/.codex/skills to getUserSkillsRoots()
so these skills appear in the web UI $ autocomplete.

Hidden directories (starting with .) inside the skills root are still
skipped (e.g., .system/).

via [HAPI](https://hapi.run)

Co-authored-by: HAPI <noreply@hapi.run>
2026-04-17 13:16:45 +08:00
Junmo KimandGitHub e6eaff83c5 fix(hub,cli): forward permissionMode on session resume (#460)
* feat(hub,cli): forward permissionMode on session resume

When a session is resumed, the cached permissionMode is now forwarded
through the Hub → Runner → CLI pipeline via a new --permission-mode
flag. Previously the mode was lost on resume, resetting to 'default'.

Each CLI flavor validates the flag value against its own allowed
permission modes (e.g. CLAUDE_PERMISSION_MODES) and rejects unknown
values. The existing --yolo flag is preserved as a shorthand.

* refactor(cli): extract buildCliArgs from startRunner

Extract the CLI argument construction logic into a standalone
exported function so it can be unit-tested independently.
No behavior change.

* test(cli): add buildCliArgs unit tests for --permission-mode

Verify that the runner correctly forwards valid permission modes
via --permission-mode, rejects invalid values, and falls back to
--yolo when no permission mode is set.

* fix(cli): let --permission-mode take precedence over --yolo

When both flags are present, --permission-mode was silently
overwritten by a later --yolo. Guard legacy flag branches with
a hasExplicitPermissionMode check so the explicit flag wins.
2026-04-15 11:13:21 +08:00
ShujakuinandGitHub 0bffb03b05 feat(cli): support optional extra headers for hub requests (#445)
* feat(cli): support extra headers for hub requests

* fix(types): normalize missing session fields to null

* refactor(cli): simplify socket extra headers config
2026-04-14 13:54:51 +08:00
Haoqing WangandGitHub 74377fafee fix(cli): prevent CLAUDE_CODE_ENTRYPOINT leak into local spawn (#452)
extractSDKMetadataAsync() calls query() which sets
CLAUDE_CODE_ENTRYPOINT='sdk-ts' on the current process env.
When claudeLocal() later spawns the claude CLI, the child
inherits this env var, causing Claude Code to treat the
session as SDK-launched. This makes the session invisible
to `claude --resume`.

Strip CLAUDE_CODE_ENTRYPOINT from the child env so the
local spawn uses its own default entrypoint.

Closes #450
2026-04-14 13:29:05 +08:00
ShujakuinandGitHub fadc0def8f fix(cli): honor HAPI_HOSTNAME in session metadata (#449) 2026-04-13 20:25:10 +08:00
tuanaiseoandGitHub 03b6a667c0 fix(security): potential command injection on windows due to `she (#439)
The process is spawned with `shell: process.platform === 'win32'` while including dynamic values (e.g., `opts.sessionId`) in `args`. On Windows, shell invocation can introduce command parsing/injection risks if arguments are not strictly validated/escaped.

Affected files: opencodeLocal.ts

Signed-off-by: tuanaiseo <221258316+tuanaiseo@users.noreply.github.com>
2026-04-11 21:37:27 +08:00
Haoqing WangandGitHub c62a1eb151 fix(cli,hub): resolve typecheck errors in codex reasoning effort (#432)
* fix(cli,hub): resolve typecheck errors in codex reasoning effort and notification test

- Cast `getModelReasoningEffort()` return (string | null) to
  `ReasoningEffort | undefined` at three call sites in
  codexLocalLauncher.ts and runCodex.ts where the narrower type is
  expected.
- Add missing `modelReasoningEffort: null` default in
  notificationHub.test.ts to satisfy the Session type contract.

These errors were introduced in 79a13d2 and have been failing CI on
main since 2026-04-10.

* fix(cli): add missing getModelReasoningEffort to test mock session

The test stub in codexLocalLauncher.test.ts was missing the
getModelReasoningEffort method added in 79a13d2, causing runtime
TypeError in CI.
2026-04-11 16:44:06 +08:00