Repository navigation
feat(tui): list every hook run in a sidebar Hooks tab - #111
Closed
Miaoranmmm wants to merge 2 commits into
Closed
Miaoranmmm wants to merge 2 commits into
Miaoranmmm wants to merge 2 commits into
Conversation
Contributor
|
head_sha: 🤖 正在生成合并请求摘要,请稍候… |
Contributor
|
head_sha: 🤖 AI 代码检视正在进行中,请稍候… |
Contributor
|
head_sha: |
1 similar comment
Contributor
|
head_sha: |
Contributor
Miaoranmmm
force-pushed
the
feat/hook-panel
branch
from
October 23, 2026 06:15
28f4647 to
89b60f3
Compare
Contributor
The footer indicator says which hook iCode is waiting on, but nothing showed what hooks ran, what they were handed, or what they returned. The engine the TUI builds now keeps a run log (HookRunLog) of every main-session hook run since iCode started, across sessions and manager replacements: the payload the hook read, its result file, stdout and stderr, and what iCode took from it (blocked, modified args, added reminder or context) or why it took nothing. The manager writes it when a run registers, takes its slot and ends, cancelled runs and runs ending after the manager closed included, and publishes HookRunsChanged. The latest 100 ended runs are kept, their details within 16 MB, payloads cut at 64 KB; nothing is written to disk. Headless and ACP keep no log. The runner keeps the output a timed-out or stopped hook printed, counts the bytes it dropped, tells a missing result file from an invalid one, writes the payload text it is handed, and returns a detached run's log path. Sub-agents pass their origin to tool, approval and pre_compact hooks, so their runs are named. The sidebar's new Hooks tab, after Context, lists the runs numbered in the order they registered, newest at the bottom, following new ones unless scrolled up, with icon, duration and result; Clear ended hides ended runs, numbers carrying on; once more than 100 runs ended, a line at the top says only the latest 100 are kept. While a dialog covers the list, changes wait until it closes. A click opens the run's Input and Output, each part a box that copies itself on a click. Clicking the footer indicator opens that tab, scrolled to the run it shows, and that run's details. The wheel over the sidebar's tab strip slides it to the tabs a narrow sidebar cuts off.
Miaoranmmm
force-pushed
the
feat/hook-panel
branch
from
October 23, 2026 07:22
89b60f3 to
c466f39
Compare
Contributor
0x7c13
force-pushed
the
feat/hook-panel
branch
from
October 23, 2026 19:30
9ba1bf9 to
74d44c3
Compare
Contributor
0x7c13
force-pushed
the
feat/hook-panel
branch
from
October 23, 2026 20:03
74d44c3 to
1c78a2e
Compare
0x7c13
force-pushed
the
feat/hook-panel
branch
4 times, most recently
from
October 23, 2026 22:49
b18b835 to
6bca371
Compare
Contributor
0x7c13
force-pushed
the
feat/hook-panel
branch
from
October 23, 2026 23:27
6bca371 to
e740b10
Compare
Contributor
Review fixes for the Hooks panel, made at their causes:
- The manager ends a run in one step (`_finish_run`), before anything is
awaited: the indicator, the run log and the trajectory span agree, an
interrupt while reporting the end no longer logs a finished async run
as cancelled, and the span still closes. A detached run interrupted
while it is handed off is recorded cancelled. A run releases its
payload once it ended. A payload JSON cannot write fails the run, not
the caller's tool call. The sub-agent is named from the invocation
origin, so it has one name on every event. A cancelled run keeps
what the hook printed until then. Changed arguments are compared as
the JSON the tool gets whenever a hook returned an override, not as
deep copies or by dict equality: arguments nested past the recursion
limit no longer strand the hooks after them, and 1 turned to true is
a change. Whether the span says the arguments changed is settled
before the run ends, so nothing raises between a run's end and its
report. Only a string names an action; any other value names none.
A pending outbox job whose payload is no object fails as it does
unrecorded, and the jobs after it still run.
A result the manager cannot take (a reason nested too deep to write
as text) ends and reports its run before the error goes on to the
caller, as it did before.
- The run log counts every text a detail keeps, what a run applied
included, toward its budget; changed arguments and block reasons are
cut at 64 KB like payloads, so the reasons ended runs keep when their
details go always fit the budget; a cut keeps the lone surrogates
of a path that is not UTF-8. A result file holding an empty
object is kept as `{}`. JSON is indented only while that keeps it
within twice its compact size (`indented_json`): a deeply nested
value, whose indentation grows with its depth on every line, stays
compact in the run log and in the dialog. Recording never raises on
what a hook wrote: JSON parses objects nested deeper than it writes
(or Python compares), and such a value is recorded as a note while
the call goes on as it would without the run log.
- The dialog holds the run's text as it is: widgets show it sanitized
(controls and lone surrogates made visible), copy takes it raw, so
multi-line shell hooks and block reasons keep their lines. A detached
log is copied as written, in the lines the Log widget keeps (CR and
CRLF end a line there as LF does, and blank lines at its end are
kept), and a tail cut into a line skips to the first break of either
kind. A CR a read ends on waits for the next read, so a CRLF split
between two reads ends one line, not two. The dialog says when the
run log dropped the run, stops polling once nothing is left to read,
shows how long a run queued (counting up while it still does), and
when a detached run was handed off. An open detached run's dialog
says its log is shared once another run of the hook writes to it
too, keeping the lines it shows. A long block shows its first and
last 500 lines (32 KB at most each), plain, around a note of what it
leaves out, and a copy takes it whole: laid out whole, 256 KB of
short lines held the TUI for 9 seconds, a 1 MB result file for 48.
The Input tab keeps the payload it shows, and copies it, when the run
log drops the run's details after the dialog opened. A detached log
shows a line's first 64 K characters and a mark where the rest goes,
which a copy keeps: the Log widget measures a line on every write.
- Each row ends with its duration at the right edge, the result before
it; a running row shows only its spinner and its time counting up.
The list's scrollbar sits on the panel's edge. Rows follow a theme
change, painted once a hidden or covered list shows. A list at its
bottom stays there when the notice that runs were dropped shows
above its rows. The empty state repaints only when its text changes,
follows the number of enabled hooks, and the list scrolls to a row
below a notice however many lines it wraps to.
- Clicking the idle indicator while the sidebar cannot open (an agent
loading, the theme editor holding it hidden) shows the configured
hooks, as a workflow does.
- A stopped hook's block reason says "stopped" ahead of its stderr.
- Tests: the secret-leak scan shuts the engine down before reading its
files (Windows held them open), detached logs are written as bytes
(text mode writes CRLF on Windows), tests that said polling stops
count the timer's ticks, and run log tests wait for fire-and-forget
runs.
0x7c13
force-pushed
the
feat/hook-panel
branch
from
October 23, 2026 23:54
e740b10 to
3c2c0d3
Compare
openjiuwenai
pushed a commit
that referenced
this pull request
Oct 24, 2026
[mirror] #111: feat(tui): list every hook run in a sidebar Hooks tab Created-by: openjiuwen-sync Commit-by: Jiaqi (0x7c13) Liu;Miaoranmmm Merged-by: Ox7c13 Description: Paired: [GitHub #111](#111) ↔ [GitCode !81](https://gitcode.com/openJiuwen/iCode/merge_requests/81) <!-- bot1-mirror-meta --> github_pr: 111 head_repo: Miaoranmmm/iCode head_ref: feat/hook-panel head_sha: e740b10 base_repo: openJiuwen-ai/iCode base_ref: main github_repo: openJiuwen-ai/iCode base_branch: main base_sha: 3081c92 github_user: Miaoranmmm <!-- /bot1-mirror-meta --> GitHub PR mirror for CodeArts CI. <!-- bot1-source-body --> ## Summary The footer indicator (#104) says which hook iCode is waiting on, but nothing showed what hooks ran, what they were handed, or what they returned. This adds a **Hooks** tab to the sidebar listing every hook run since iCode started, and a details dialog with each run's input and output. ## What the TUI shows - **Hooks tab** (after Context): every main-session run since iCode started, across sessions, as one timeline. Each row shows: - the run number, in registration order, oldest at the top and newest at the bottom; - an icon; - the hook, with the tool or event, and the sub-agent when one fired it; - the duration, ticking while queued or running; - the result: `exit 1`, `timed out`, `blocked`, `modified args`, `added reminder`, `added context`, `cancelled`, `detached` and so on. - **Following and clearing:** the list follows new runs while it sits at the bottom and stays put when scrolled up. The title counts active runs (`Hooks (2 active)`). - **Clear ended** hides ended runs, the same way Background Tasks' Clear ended does, and numbers carry on. - Once more than 100 runs have ended, a line at the top says only the latest 100 are kept. - Empty states: no hooks configured; nothing ran yet; all ended runs cleared. - **Details dialog:** clicking a row opens Input (the payload, how the hook was run) and Output (stdout, stderr, the result file, and what iCode took from it, or why it took nothing). Each part is a box that copies itself on a click. A running run opens on Input and fills Output when it ends. - **Footer indicator:** clicking it opens the tab, scrolled to the run it shows with that row marked, plus that run's details. - **Behind a dialog:** while a dialog covers the list, its timer stops and changes wait until the dialog closes. Opening the tab or a run reads at once. - **Narrow sidebar:** the wheel over the sidebar's tab strip slides it to the tabs that are cut off. ## How it works - **The run log.** `service/hooks/run_log.py::HookRunLog` keeps the runs in this process only; nothing goes to disk, the session file, the trajectory or the logs, since payloads can hold secrets from tool arguments. - It belongs to the engine the TUI builds (`record_hook_runs=True`), so session switches and manager rebuilds keep it. Headless and ACP keep no log. - It keeps the latest 100 ended runs, with their details within 16 MB and payloads cut at 64 KB. Queued and running runs are always kept. - `HookRunLog.dropped` counts ended runs dropped past the cap; that count drives the notice. - **The manager** writes a run at every point it already tracks: registration (`_run_blocking`, `_start_tracked`), taking a slot, the end, and cancellation, including a run cancelled before its first step. `_settle_unstarted` ends those, from the drain's `finally` and from `_reap_background`. Runs ending after the manager closed are recorded too. The payload is serialized when a run registers, because a later blocking hook's argument override rewrites the same dict in place. - Each change publishes `HookRunsChanged`, which is TUI only. - What a blocking run applied is worked out where its decision is merged (`_blocking_effect`); an `async` or `fire_and_forget` run's decision is never taken (`_nonblocking_applied`). - `HookExecutionView` gains a `run_id`, so the indicator can open the run it shows. - **The runner:** - keeps the output a timed-out or stopped hook printed, keeping a head and tail and counting the dropped bytes; - tells a missing result file from an invalid one; - writes the payload text it is handed; - returns a detached run's log path. - **Sub-agents** pass their origin to tool, approval and `pre_compact` hooks, so their runs name the sub-agent. ## Docs and i18n - The hooks guide (en, zh-Hans) describes the tab, the details dialog and what clicking the indicator opens. - New strings are translated for zh-Hans, and the catalog oracle is updated. `trajectory_wait_manifest.json` is refreshed. ## Tests Written from the design, independently of the implementation: - **Run log** (`test_run_log*.py`, `test_runner_capture.py`): - registration, start and end; - retention, the detail cap and `dropped`; - cancelled and unstarted runs; - runs ending after close; - output kept from timed-out hooks, and result-file states; - what each run applied, from real hook subprocesses. - **Hooks list** (`test_hook_run_list.py`): - rows, icons and results; - order and following; - Clear ended, with numbers carrying on; - the notice only past the cap, and not selectable; - the three empty states. - **Panel flows** (`test_hooks_panel_flow.py`, `test_hook_run_modal.py`, real app): - the tab, the details dialog and copy; - the indicator opening the run and scrolling to its row; - sessions mixed into one timeline; - the timer stopping under a dialog and catching up; - changes under a dialog read once it closes; - no MainScreen restyle, relayout or Footer recompose on a populated transcript. - **Engine-driven** (`test_hook_run_engine.py`, `test_hook_run_reporting.py`): runs of the main agent and of sub-agents reach the log with their origin, and `HookRunsChanged` is published. Verified locally on macOS on top of current main: - ruff, ruff format and ty (linux, darwin, win32) pass. - The i18n pipeline passes, and a second run changes nothing. - The Hooks, `screens/main`, hooks, i18n and changed test files pass (1913), and so does `tests/architecture` (1240). - Smart Test fails only on `test_package_trust.py::test_previews_and_runs_keep_bytecode_only_in_the_private_cache_of_the_config_dir`, which fails the same way on main in this checkout. - Exercised by hand in the TUI with results, concurrent and queued runs, cancellation, sub-agents, session switches and over 100 runs. <!-- /bot1-source-body --> See merge request: openJiuwen/iCode!81
Contributor
|
🤖 This pull request has been merged, and the target branch is up to date. |
3 tasks done
openjiuwen-release-bot Bot
pushed a commit
that referenced
this pull request
Oct 24, 2026
fix(hooks): keep a path's lone surrogates when a run log text is cut Created-by: Ox7c13 Commit-by: Jiaqi (0x7c13) Liu Merged-by: Ox7c13 Description: Paired: [GitHub #116](#116) ↔ [GitCode !83](https://gitcode.com/openJiuwen/iCode/merge_requests/83) ## Summary !81 (GitHub #111) merged the Hooks panel at `e740b102`, one push before its final head `3c2c0d33`. This MR brings in that last push, which is three files: - **`service/hooks/run_log.py`:** `_clip_text` no longer decodes the cut with `"ignore"`. That dropped every lone surrogate in a changed-arguments or block-reason text over 64 KB. A path that is not UTF-8 holds such surrogates. Now the cut only drops the character the limit splits: it backs up to that character's first byte and decodes with `"surrogatepass"`. - **`tests/service/hooks/test_run_log_retention.py`:** the cap test now checks every unit, including a lone surrogate. The kept text must start the full text and end within 4 bytes of the limit. - **`tests/orchestration/engine/test_hook_run_reporting.py`:** removes a trailing `wait_until` that could not fail. The tree is identical to `3c2c0d33`, whose CI passed in full. ## Test plan - [x] `test_changed_arguments_past_the_cap_are_cut_with_their_size_kept[lone-surrogate]` fails against `main` and passes with this change. - [x] Smart Test `--paths` on the three files: 12589 passed, 10 skipped; architecture 390 passed. - [x] `ruff check`, `ruff format --check`, and `ty check` (linux, darwin, win32). - [x] GitHub CI on this exact commit (`de64d5eb`, the closed GitHub #115): all 17 checks passed. <!-- bot4-pr-sync-meta schema_version: 1 source: gitcode gitcode_repo: openJiuwen/iCode gitcode_mr: 83 github_repo: openJiuwen-ai/iCode github_pr: 116 github_branch: sync/pr-83 --> See merge request: openJiuwen/iCode!83
openjiuwen-release-bot Bot
pushed a commit
that referenced
this pull request
Oct 26, 2026
[mirror] #124: chore(release): bump version to 0.32.0 Created-by: openjiuwen-sync Commit-by: Jiaqi (0x7c13) Liu Merged-by: Ox7c13 Description: Paired: [GitHub #124](#124) ↔ [GitCode !91](https://gitcode.com/openJiuwen/iCode/merge_requests/91) <!-- bot1-mirror-meta --> github_pr: 124 head_repo: 0x7c13/iCode head_ref: dev/release-0.32.0 head_sha: c9d1882 base_repo: openJiuwen-ai/iCode base_ref: main github_repo: openJiuwen-ai/iCode base_branch: main base_sha: e225cb1 github_user: 0x7c13 <!-- /bot1-mirror-meta --> GitHub PR mirror for CodeArts CI. <!-- bot1-source-body --> Bump iCode to 0.32.0 so a release is cut from current `main`. ## Changes - `pyproject.toml` and `uv.lock`: the package version moves from 0.31.0 to 0.32.0. - i18n catalogs (`locales/chrys.pot`, `locales/zh-Hans/LC_MESSAGES/chrys.po`, the compiled `chrys.mo`): the `Project-Id-Version` header now says 0.32.0. No message changes. ## Release Merging this changes the version in `pyproject.toml` on `main`, so `tag-release.yml` creates the `v0.32.0` tag and starts `cd.yml` for that tag. This is the first release whose Linux offline binaries bundle Pillow 13.0.0, from the glibc 2.17 wheels published as `pillow-13.0.0-manylinux2014.1` (#122, #123). ## Included since 0.31.0 - #123 Pillow 13.0.0 and pypdf 6.21.2 (pypdf security fixes); the Linux offline binaries take the matching glibc 2.17 Pillow wheels - #122 the manylinux2014 Pillow wheel workflow builds 13.0.0 on patched build images, with every source archive pinned by SHA-256 (build-only); hook indicator tests stabilized - #121 completed turns fold into activity sections; the docs show how to request readable Claude thinking summaries - #120 the prompt editor asks for confirmation before its Discard button closes it - #111 a sidebar Hooks tab lists every hook run, and each run is recorded once - GitCode !85 the main chat shows the model's thinking as it streams; a resumed session replays it folded, and `/fold` folds it too - GitCode !84 the chat follows new content only while you are at the bottom - GitCode !83 a hook run log cut to size keeps file paths that are not valid UTF-8 intact - GitCode !82 faster startup: a loading screen with the logo paints before the main screen and agent load, and F12 works again once the agent is ready ## Verification - `uv lock --check` passes. - `scripts/i18n.py extract/update/compile/check` runs clean, and a second run changes nothing. - Tests were not run locally; CI covers them. <!-- /bot1-source-body --> See merge request: openJiuwen/iCode!91
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Paired: GitHub #111 ↔ GitCode !81
Summary
The footer indicator (#104) says which hook iCode is waiting on, but nothing showed what hooks ran, what they were handed, or what they returned. This adds a Hooks tab to the sidebar listing every hook run since iCode started, and a details dialog with each run's input and output.
What the TUI shows
exit 1,timed out,blocked,modified args,added reminder,added context,cancelled,detachedand so on.Hooks (2 active)).How it works
service/hooks/run_log.py::HookRunLogkeeps the runs in this process only; nothing goes to disk, the session file, the trajectory or the logs, since payloads can hold secrets from tool arguments.record_hook_runs=True), so session switches and manager rebuilds keep it. Headless and ACP keep no log.HookRunLog.droppedcounts ended runs dropped past the cap; that count drives the notice._run_blocking,_start_tracked), taking a slot, the end, and cancellation, including a run cancelled before its first step._settle_unstartedends those, from the drain'sfinallyand from_reap_background. Runs ending after the manager closed are recorded too. The payload is serialized when a run registers, because a later blocking hook's argument override rewrites the same dict in place.HookRunsChanged, which is TUI only._blocking_effect); anasyncorfire_and_forgetrun's decision is never taken (_nonblocking_applied).HookExecutionViewgains arun_id, so the indicator can open the run it shows.pre_compacthooks, so their runs name the sub-agent.Docs and i18n
trajectory_wait_manifest.jsonis refreshed.Tests
Written from the design, independently of the implementation:
test_run_log*.py,test_runner_capture.py):dropped;test_hook_run_list.py):test_hooks_panel_flow.py,test_hook_run_modal.py, real app):test_hook_run_engine.py,test_hook_run_reporting.py): runs of the main agent and of sub-agents reach the log with their origin, andHookRunsChangedis published.Verified locally on macOS on top of current main:
screens/main, hooks, i18n and changed test files pass (1913), and so doestests/architecture(1240).test_package_trust.py::test_previews_and_runs_keep_bytecode_only_in_the_private_cache_of_the_config_dir, which fails the same way on main in this checkout.