mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-08 07:17:39 +00:00
2ce6d3ef3a25d80932e9de4e209ffa9a0a9880c1
5
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
2ce6d3ef3a |
feat(web): show tool call duration in the detail dialog (#1036)
* refactor(web): export formatDuration for reuse * feat(web): show tool call duration in the detail dialog Show a completed tool's execution duration at the top of its detail dialog. The value is derived from the Claude entry's own timestamps (the execution machine's wall clock) rather than the hub's message-receive time, and is used only when both the tool_use and tool_result entries carry a real timestamp — otherwise it falls back to the hub receive times on both sides, so the two clocks are never mixed. Running/pending tools show nothing, the running-state live timer is unchanged, and clock skew is guarded against. Reuses the existing formatDuration formatter. No schema changes. * fix(web): backfill hub startedAt on reorder so duration isn't 0.0s When a tool_result entry is reduced before its tool_use, the tool block is created from the result, so the hub startedAt is the result receive time. The tool_use path only lowered the exec start, not the hub startedAt, so a timestamp-less pair (no exec duration available) fell back to startedAt === completedAt and the detail dialog showed 0.0s. Lower the hub startedAt to the earlier tool_use receive time as well. |
||
|
|
763f45acdd |
feat: add support for Kimi Code CLI and fixed some bugs (#659)
* Add Kimi agent support via ACP protocol Add full integration for the Kimi Code CLI agent using the standard Agent Client Protocol (ACP). Includes: - kimi command and CLI registry wiring - Local launcher spawning kimi directly - Remote launcher with ACP stdio transport via AcpSdkBackend - Session management with resume support - Permission handler supporting all Kimi permission modes - Terminal UI display component - Runtime config resolving model from env and ~/.kimi/config.toml via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Fix Kimi ACP tool call input decoding on web Kimi streams tool arguments as JSON text inside the content array (e.g. {\"command\": \"df -h\"}) instead of rawInput/kind. The handler now extracts input from three sources in priority order: 1. rawInput (Claude/Codex path) 2. kind + title fallback (Gemini path) 3. content JSON text (Kimi path) Also handles: - rawInput: null no longer blocks the kind+title fallback - Title prefixes like \"Shell: free -h\" are stripped to extract args - Stale placeholder inputs are re-derived when the title updates - Normalized kind aliases (shell, run, read_file, write, etc.) via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * Add kimi support to web UI * Fix some bugs * fix(kimi): dedupe repeated tool_call display in terminal UI * fix(web): keep tool block immutable so React detects input/state changes * fix(web): recognise Kimi subagent titles like 'Agent: ...' as subagent tools * fix(web): allow-for-session for ACP agents (kimi, cursor) PermissionFooter treated all non-codex sessions as Claude, sending Claude-specific acceptEdits/allowTools to ACP agents that don't support them. Hub rejected acceptEdits for kimi, and the ACP PermissionAdapter ignored allowTools. - Only show 'allow all edits' for Claude sessions - Send decision: approved_for_session for non-Claude ACP agents - Update status display to check decision field * fix(web): lookup subagent sidechains by tool-call id instead of msg id * fix(web): don't trim newest messages when loading older history fetchOlderMessages was using trimVisible(merged, 'prepend') which kept the oldest 400 messages and dropped the newest ones. This caused: 1. Latest messages to disappear when user loaded older history 2. User to see no visible change when new old messages were drowned in the 400-message window. Remove the incorrect trim so all fetched older messages are retained alongside the current window. Subsequent ingestIncomingMessages (append mode) will naturally keep the window bounded when new agent messages arrive. * fix(cli): route Kimi session resume to runKimi instead of runCursor Kimi was present in AGENT_FLAVORS but dispatchLocalResume had no branch for it, so resuming a Kimi session fell through to the Cursor launcher. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(cli): pass selected model to Kimi ACP backend via KIMI_MODEL env createKimiBackend was ignoring opts.model and only setting KIMI_PROJECT_DIR. Use buildKimiEnv so the selected model reaches the subprocess as KIMI_MODEL. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): bound message window on older loads with dedicated larger cap fetchOlderMessages was keeping all messages unbounded, causing sessionStorage bloat on repeated pagination. Reintroduce trimming with OLDER_LOAD_WINDOW_SIZE (800) so growth is capped while the newest messages are still preserved for far longer than before. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(web): revert sidechain lookup to message id, matching tracer/grouping pipeline tracer.ts sets sidechainId to the parent message id, and reducer.ts groups by sidechainId. A prior commit changed reducerTimeline.ts to look up by tool-call id (c.id), which broke sidechain attachment. Revert to msg.id so the lookup matches the actual grouping key end-to-end. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(cli): gate ACP title prefix stripping to known tool-kind labels extractTitleArgument stripped at the first colon unconditionally, corrupting commands/paths like curl http://localhost:3000 or Windows paths. Now it only strips when the prefix normalizes to the same tool kind as the event, verified via regex. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> * fix(shared): include kimi in isCodexFamilyFlavor for ACP permission UI Kimi is an ACP-style agent that supports the abort decision, but isCodexFamilyFlavor excluded it, so PermissionFooter rendered the non-Codex Allow/Deny UI without the Abort button. via [HAPI](https://hapi.run) Co-Authored-By: HAPI <noreply@hapi.run> --------- Co-authored-by: HAPI <noreply@hapi.run> |
||
|
|
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.
|
||
|
|
c50621e85a |
feat: add MCP config and system prompt support for Codex local mode
Implements MCP server configuration and developer instructions support in Codex local mode, bringing it to parity with remote mode. Key additions: - buildMcpServerConfigArgs() and buildDeveloperInstructionsArg() utilities to construct -c config arguments for passing MCP servers and instructions to the Codex CLI at runtime - TITLE_INSTRUCTION for Codex to call hapi__change_title to update chat session titles dynamically - Codex local mode now starts hapi MCP bridge server and passes both MCP configuration and developer instructions to Claude, enabling full feature parity with remote mode Files changed: - New: codexMcpConfig.ts (utilities), systemPrompt.ts (prompt definition), codexMcpConfig.test.ts (comprehensive tests) - Updated: codexLocal.ts (accepts mcpServers, builds config args), codexLocalLauncher.ts (starts hapi server, passes config), codexStartConfig.ts (imports TITLE_INSTRUCTION) |
||
|
|
34cfe49f87 |
refactor: extract chat normalization and reducer logic, cleanup legacy credentials
- Split normalize.ts into focused modules: normalizeAgent, normalizeUser, normalizeUtils - Extract reducer.ts into specialized modules: reducerCliOutput, reducerEvents, reducerTimeline, reducerTools - Remove legacy credentials support (zod schema, readCredentials, writeCredentialsLegacy) - Refactor NewSession component from monolithic file into modular subcomponents - Improves code organization and maintainability across chat and authentication layers |