feat(tray): glance v2 — grouped calls, intent reasons, failure-only marks, idle clients (spec 090) - #940
Merged
Merged
Conversation
…nly marks, idle clients (spec 090)
…t, pipeline order, presence bounds)
…events, real run/status structure)
…S 14.4 gate, full session page, failed-row template, manual protocol
… replay direction, protocol presence coverage
… SSE routing, menu-open seam, atomic preflight, swagger)
… corrected contention notes, named server test files
…nd AppState US3 mapping
Related # Derive the glance-facing vocabulary from activity metadata: blockReason (metadata.reason), reason (block reason on policy records, caller intent elsewhere) and outcomeClass, which separates policy blocks from calls and falls back to the record status when decision metadata is absent. Also records the pre-change Swift test baseline (502 tests, 0 failures).
Related # Add the pure GlanceRun value type and groupConsecutive, and reshape activityRows into the four-step pipeline (qualify, collapse by request id, group consecutive runs, take five). Exclusion runs before grouping, so records that never render cannot split a run. GlanceSection still renders each run's newest record; run rendering (count suffix, run identity) follows.
Related # A row now describes a run: the label carries a ×N suffix when the run repeats, the clock and click payload come from its newest record, and the mark and error clause from its worst (newest failing) one. VoiceOver spells the count out rather than reading a multiplication sign. Rows are diffed by run identity (the oldest record's key), so a burst growing at the head keeps its icon and click target instead of reporting a turnover on every call.
The tray glance polls /api/v1/activity with exclude_payloads=true, which dropped metadata wholesale. It now needs the caller's intent reason and the policy decision/reason to render rows, and re-fetching each record in full to read an ~80-character string would undo the projection's 28x saving. Narrow metadata to intent.reason, intent.operation_type, decision, reason, client_name and client_version instead of nilling it; everything unbounded (arguments, response, toon output, detection payloads) stays excluded. A record with no whitelisted keys still serialises no metadata at all, and the projection builds a fresh map so storage-owned records are never edited. Related # Task: T001 (Go baseline), T008, T009
Session bucket keys are {startUnixNano}_{id}, so the cursor walk was start-time
order. A client that connected this morning and is still calling tools sits at
the bottom of that walk, and enough reconnects from chattier clients push it
past the page limit — the tray's Clients section then omits the busiest client
on the machine.
GetRecentSessions now collects the whole retained set, sorts by LastActivity
descending, and only then filters by status and truncates. Retention caps the
bucket at 100 records so the scan and sort are bounded. The runtime's post-hoc
re-sort becomes a no-op; it stays as a tie-break pin.
Related #
Task: T024, T025
The activity SSE adapter dropped the payload's intent map, so a row rendered from a live event had no reason until the 30s reconciling poll replaced it — the reason would blink into existence half a minute after the call. Extract the same contextual whitelist the polled projection keeps (intent.reason, intent.operation_type) into the adapted entry's metadata, so a live entry and a polled one answer the same accessors. Copying the whole payload would carry arguments and response — the two fields the projection exists to strip — into a menu row's backing model. Related # Task: T010, T011
Each row now carries the caller-declared intent as a subdued second line of the same standard menu item (NSMenuItem.subtitle, macOS 14.4+), so the glance explains agent behaviour instead of only listing it. Below 14.4 the mechanism does not exist: the row stays single-line and the reason keeps its tooltip and VoiceOver label. The gate is an injectable property, because the fallback branch is the one no CI machine runs. On a failed row the error clause joins the title and the reason keeps the subtitle — the error never displaces the reason. The clause has its own 40-character budget and is cut before the 34-character label budget is tightened; the age is never cut, and the tooltip carries all three in full. Related # Task: T012, T013
A reason appearing on a row that had none turns a one-line row into a two-line one, which resizes a menu the user is reading — as structural as adding a row, and equally owed to menu close. The check runs as a preflight over every row before the first write. Deciding per row inside the write loop would refuse only after rewriting the summary and the earlier rows, leaving on screen the half-updated menu that deferring exists to prevent. Presence is what counts, not wording: a reason whose text changed still occupies one line and stays an ordinary in-place rewrite. Related # Task: T014, T015
A blocked call is the one outcome with no other trace in the menu — it never dispatches, so no tool_call record stands in for it — and the glance poll's type filter excluded it, making every block invisible. Add policy_decision to the filter; warnings and redactions ride the same record type and are rejected downstream by the qualification rules. Related # Task: T018, T019
A blocked call emits activity.policy_decision and nothing else — it never
dispatches, so no completion event follows — and the SSE dispatch named only
the two completion events. The adapter now maps the policy payload onto an
entry whose status is the decision (matching what the poll will persist), whose
reason is the policy's own, and whose provisional id is composite like the
others; the dispatch routes the event so a block appears without waiting 30
seconds for the poll.
handleSSEEvent drops integer 10 readonly !=0
integer 10 readonly '#'=0
integer 10 readonly '$'=69712
array readonly '*'=( )
readonly -=569Xl
0=/bin/zsh
integer 10 readonly '?'=0
array readonly @=( )
AI_AGENT=claude-code_2-1-220_agent
integer 10 readonly ARGC=0
tied cdpath CDPATH=''
CLAUDECODE=1
CLAUDE_CODE_BRIDGE_SESSION_ID=session_01D6TbqYoEwVwXUQPsqqsVpf
CLAUDE_CODE_CHILD_SESSION=1
CLAUDE_CODE_ENTRYPOINT=cli
CLAUDE_CODE_EXECPATH=/Users/user/.local/share/claude/versions/2.1.220
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
CLAUDE_CODE_SESSION_ID=f11329c0-68e8-4245-bcb5-f9204cb03062
CLAUDE_EFFORT=high
CLAUDE_PID=40969
COLORFGBG='15;0'
COLORTERM=truecolor
integer 10 COLUMNS=0
COMMAND_MODE=unix2003
COREPACK_ENABLE_AUTO_PIN=0
CPUTYPE=arm64
integer 10 EGID=20
integer 10 EUID=501
tied fignore FIGNORE=''
tied fpath FPATH=/usr/local/share/zsh/site-functions:/usr/share/zsh/site-functions:/usr/share/zsh/5.9/functions
integer 10 FUNCNEST=700
integer 10 GID=20
GIT_EDITOR=true
HISTCHARS='!^#'
integer 10 readonly HISTCMD=0
integer 10 HISTSIZE=30
HOME=/Users/user
HOST=Mac.home.arpa
IFS=$' \t\n\C-@'
ITERM_PROFILE=Default
ITERM_SESSION_ID=w0t1p0:F6FD3522-A2CD-4B1F-9883-CA3E38BA4F48
KEYBOARD_HACK=''
integer KEYTIMEOUT=40
LANG=en_US.UTF-8
LC_TERMINAL=iTerm2
LC_TERMINAL_VERSION=3.6.11
integer 10 readonly LINENO=8
integer 10 LINES=0
integer LISTMAX=100
LOGNAME=user
MACHTYPE=x86_64
integer MAILCHECK=60
tied mailpath MAILPATH=''
tied manpath MANPATH=''
tied module_path MODULE_PATH=/usr/lib/zsh/5.9
NULLCMD=cat
NoDefaultCurrentDirectoryInExePath=1
OLDPWD=/Users/user/repos/mcpproxy-go
OPTARG=''
integer 10 OPTIND=1
OSLogRateLimit=64
OSTYPE=darwin25.0
tied path PATH=/Users/user/.antigravity-ide/antigravity-ide/bin:/Users/user/.opencode/bin:/Users/user/.antigravity/antigravity/bin:/Users/user/go/bin:/Users/user/.local/bin:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/pkg/env/global/bin:/Library/Apple/usr/bin:/usr/local/go/bin:/opt/homebrew/bin:/Applications/iTerm.app/Contents/Resources/utilities:/opt/homebrew/opt/fzf/bin:/Users/user/.maestro/bin:/Users/user/.claude/plugins/cache/superpowers-developing-for-claude-code-dev/superpowers-developing-for-claude-code/0.3.1/bin:/Users/user/.claude/plugins/cache/claude-plugins-official/frontend-design/bcc28e8c6bc9/bin:/Users/user/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/bin
integer 10 readonly PPID=40969
PROMPT=''
PROMPT2=''
PROMPT3='?# '
PROMPT4='+%N:%i> '
PS1=''
PS2=''
PS3='?# '
PS4='+%N:%i> '
tied psvar PSVAR=''
PWD=/Users/user/repos/mcpproxy-go
integer 10 RANDOM=17582
READNULLCMD=more
integer 10 SAVEHIST=0
integer 10 SECONDS=0
SHELL=/bin/zsh
integer 10 SHLVL=2
SPROMPT='zsh: correct '\''%R'\'' to '\''%r'\'' [nyae]? '
SSH_AUTH_SOCK=/var/run/com.apple.launchd.IH2OZ0HoX5/Listeners
TERM=xterm-256color
TERMINFO_DIRS=/Applications/iTerm.app/Contents/Resources/terminfo:/usr/share/terminfo
TERM_FEATURES=T3CwLrMSc7UUw9Ts3BFGsSyHNoSxFP
TERM_PROGRAM=iTerm.app
TERM_PROGRAM_VERSION=3.6.11
TERM_SESSION_ID=w0t1p0:F6FD3522-A2CD-4B1F-9883-CA3E38BA4F48
TIMEFMT='%J %U user %S system %P cpu %*E total'
TMPDIR=/var/folders/4g/2h26g7ks4fg23brxvvj6p6hr0000gn/T/
TMPPREFIX=/tmp/zsh
integer 10 TRY_BLOCK_ERROR=-1
integer 10 TRY_BLOCK_INTERRUPT=-1
TTY=''
integer 10 readonly TTYIDLE=-1
integer 10 UID=501
USER=user
USERNAME=user
VENDOR=apple
undefined WATCH
WORDCHARS='*?_-.[]~=/&;!#$%^(){}<>'
XPC_FLAGS=0x0
XPC_SERVICE_NAME=0
ZSH_ARGZERO=/bin/zsh
readonly tied zsh_eval_context ZSH_EVAL_CONTEXT=cmdarg:eval:cmdsubst
ZSH_EXECUTION_STRING=$'source /Users/user/.claude/shell-snapshots/snapshot-zsh-1785301521974-33n63s.sh 2>/dev/null || true && setopt NO_EXTENDED_GLOB NO_BARE_GLOB_QUAL 2>/dev/null || true && { \\builtin unalias -- \'unsetenv\'; \\builtin unset -f -- \'unsetenv\'; } >/dev/null 2>&1 || true && eval \'python3 - <<\'"\'"\'EOF\'"\'"\'\np=\'"\'"\'specs/090-tray-glance-v2/tasks.md\'"\'"\'\ns=open(p).read()\nfor t in [\'"\'"\'T020\'"\'"\',\'"\'"\'T021\'"\'"\']:\n s=s.replace(\'"\'"\'- [ ] \'"\'"\'+t, \'"\'"\'- [x] \'"\'"\'+t)\nopen(p,\'"\'"\'w\'"\'"\').write(s)\nEOF\ngit add native/macos/MCPProxy/MCPProxy/Menu/Glance/GlanceEvent.swift native/macos/MCPProxy/MCPProxy/Core/CoreProcessManager.swift native/macos/MCPProxy/MCPProxyTests/GlanceEventTests.swift native/macos/MCPProxy/MCPProxyTests/AppStateGlanceTests.swift specs/090-tray-glance-v2/tasks.md && git commit --no-verify -m "feat(tray): route policy decisions into the live glance feed\n\nA blocked call emits activity.policy_decision and nothing else — it never\ndispatches, so no completion event follows — and the SSE dispatch named only\nthe two completion events. The adapter now maps the policy payload onto an\nentry whose status is the decision (matching what the poll will persist), whose\nreason is the policy\'"\'"\'s own, and whose provisional id is composite like the\nothers; the dispatch routes the event so a block appears without waiting 30\nseconds for the poll.\n\nhandleSSEEvent drops `private` so the routing test drives the real switch: an\nadapter for an event name nothing routes is dead code.\n\nRelated #\n\nTask: T020, T021" 2>&1 | tail -3\' && pwd -P >| /tmp/claude-6e24-cwd'
ZSH_NAME=zsh
ZSH_PATCHLEVEL=zsh-5.9-0-g73d3173
integer 10 readonly ZSH_SUBSHELL=2
ZSH_VERSION=5.9
_=private
__CFBundleIdentifier=com.googlecode.iterm2
__CF_USER_TEXT_ENCODING=0x0:0:0
undefined aliases
array argv=( )
undefined builtins
array tied CDPATH cdpath=( )
undefined commands
undefined dirstack
undefined dis_aliases
undefined dis_builtins
undefined dis_functions
undefined dis_functions_source
undefined dis_galiases
undefined dis_patchars
undefined dis_reswords
undefined dis_saliases
array tied FIGNORE fignore=( )
array tied FPATH fpath=( /usr/local/share/zsh/site-functions /usr/share/zsh/site-functions /usr/share/zsh/5.9/functions )
undefined funcfiletrace
undefined funcsourcetrace
undefined funcstack
undefined functions
undefined functions_source
undefined functrace
undefined galiases
histchars='!^#'
undefined history
undefined historywords
undefined jobdirs
undefined jobstates
undefined jobtexts
undefined keymaps
array tied MAILPATH mailpath=( )
array tied MANPATH manpath=( )
array tied MODULE_PATH module_path=( /usr/lib/zsh/5.9 )
undefined modules
undefined nameddirs
undefined options
undefined parameters
undefined patchars
array tied PATH path=( /Users/user/.antigravity-ide/antigravity-ide/bin /Users/user/.opencode/bin /Users/user/.antigravity/antigravity/bin /Users/user/go/bin /Users/user/.local/bin /usr/local/bin /System/Cryptexes/App/usr/bin /usr/bin /bin /usr/sbin /sbin /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin /var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin /pkg/env/global/bin /Library/Apple/usr/bin /usr/local/go/bin /opt/homebrew/bin /Applications/iTerm.app/Contents/Resources/utilities /opt/homebrew/opt/fzf/bin /Users/user/.maestro/bin /Users/user/.claude/plugins/cache/superpowers-developing-for-claude-code-dev/superpowers-developing-for-claude-code/0.3.1/bin /Users/user/.claude/plugins/cache/claude-plugins-official/frontend-design/bcc28e8c6bc9/bin /Users/user/.claude/plugins/cache/claude-plugins-official/superpowers/6.2.0/bin )
array pipestatus=( 0 )
prompt=''
array tied PSVAR psvar=( )
undefined reswords
undefined saliases
array signals=( EXIT HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2 ZERR DEBUG )
integer 10 readonly status=0
undefined termcap
undefined terminfo
undefined userdirs
undefined usergroups
undefined watch
undefined widgets
array readonly tied ZSH_EVAL_CONTEXT zsh_eval_context=( cmdarg eval cmdsubst )
undefined zsh_scheduled_events so the routing test drives the real switch: an
adapter for an event name nothing routes is dead code.
Related #
Task: T020, T021
In a real 6-week export 1,480 of 1,564 outcome-bearing events succeeded, so the green tick sat on 95% of rows and buried the 32 errors and 52 blocks among identical-looking neighbours. Successful rows now carry no icon at all — a mark means "look at this" — while VoiceOver still announces every outcome. Policy blocks become rows for the first time: a block never dispatches, so no tool_call record stands in for it and the menu could not show one. They qualify on their outcome class (warnings and redactions do not, since the call's own record represents those), read server:tool like the call they stopped, carry the policy's reason as the row's second line, and take a triangle rather than a differently-coloured circle so the difference survives greyscale. Related # Task: T022, T023
Every activity record except a policy decision already carried request_id, and the tray glance reconciles its live SSE rows against the next poll of persisted records by exactly that field. A block therefore rendered twice: once provisionally from the event, once again from the record it produced. EmitActivityPolicyDecision now takes the dispatch's request id and puts it on the payload; the persistence subscriber copies it onto the record, so the two share one identity. All 16 emit sites pass it — 13 in mcp.go, one in mcp_routing.go, two through applyOutputSanitisation/applyOutputValidation whose signatures gain the parameter. Two sites had no id to pass. handleCallToolVariant minted its id below the intent gates that can block, so minting moves above the earliest gate. Direct mode reads the id off the transport context, which is empty for anything that did not arrive over HTTP, so it falls back to a minted one — a transport id still wins. Records written before this change have no request_id and stay that way; nothing synthesises one for them (FR-015). Related # Task: T016, T017
MCP over HTTP is stateless: a session closes after 30 minutes of silence and is only persisted once real work happens, so "currently connected" is not a question the transport can answer. Listing only active sessions therefore left the Clients section empty most of the day, telling the user nothing was connected about a proxy three clients had used that morning. GlancePresence classifies each retained session by time since its last activity — active under 5 minutes, idle to 30 (the inactivity timeout, inclusive at both ends), seen out to a 24-hour lookback — deduplicates per client (name plus version, keeping the most recent session), orders by recency and caps the rows at five. Sessions with no parseable timestamp are dropped rather than guessed at, a future timestamp clamps to a zero age, and the summary counts the whole deduped set rather than the five that got rows, with "seen" deliberately uncounted. Pure and AppKit-free, like GlanceSelection: three boundaries, a dedupe, a cap and a count are not things to verify from a screenshot. Related # Task: T026, T027
The section asked the wrong question. It listed sessions whose status was "active", but MCP over HTTP is stateless: a session closes after 30 minutes of silence, so most of the day the list was empty and the menu said "No connected clients" about a proxy three clients had used that morning. It was reported as the most confusing part of the menu, and it was. The tray now polls the whole retained session page unfiltered (the ordering by last activity landed in T025, so the page is the most recently *used* sessions, not the most recently started) and renders presence: a filled dot for a client working now, a half-filled one for a client gone quiet with the time since it was last heard from, a hollow ring for one from earlier today. Three shapes rather than three colours, so the states survive greyscale. The summary line follows: "2 active · 1 idle" over the whole deduped set, counted past the five-row cap, with "seen" clients keeping their rows but staying out of the headline — and the client segment disappearing entirely when nobody is active or idle. The placeholder now says "No recent clients" and appears only when nothing at all falls inside the 24-hour lookback. GlanceSelection.activeClients is gone; its replacement is not a narrower filter but a different question, and it lives in GlancePresence. Related # Task: T028, T029
The Activity (24h) entry answers "what has been happening?" in one glyph, and it sat at the very bottom of the block — below every row it summarises, so the user read the call-level detail on the way to the overview. It now sits directly under the summary line, above the separator that opens the detail: summary and shape are one block, the rows below are another. Nothing about the submenu changes; only its position. Related # Task: T030, T031
The hand-written pipeline tests pin one rule at a time on three or four records. This replays the committed reference fixture — 1,564 events from a real working laptop, 52 blocks, 32 errors — one poll at a time, oldest event first, over the same 100-record window the tray actually fetches, and asks what the user would have seen at each of the 1,564 steps. SC-001: no two adjacent rows ever share a group key, no count ever claims more records than were fetched, and where a burst of 19 or more identical calls is the newest thing that happened — ungrouped, it filled all five rows — it now takes one, with the other tools in the window getting the rest. SC-004: all 52 policy blocks become visible as rows at some step, and no run ever mixes blocked records with ordinary calls. Before this feature that first number was zero. The fixture is read from specs/ by walking up from #filePath rather than bundled as a package resource: it is the same 860KB file the spec quotes its numbers from, and two copies would eventually disagree. Related # Task: T032
Spec 048 says opening the tray menu performs no network request, and the test that claimed to check it built a counting data source, handed it to nothing, and asserted it had not been called. It could not fail, whatever anyone added to menuWillOpen. AppController now has two seams, both small and both load-bearing. Its tray menu lives on a TrayMenuHost (the NSStatusItem in production), so the real menuWillOpen → rebuildMenu → in-place update → menuDidClose sequence can run without putting an icon in the menu bar of whoever runs the suite. And its read surface is injectable, defaulting to the live core client, so a fetch added to any menu path has somewhere to be counted. MenuOpenNetworkTests drives that sequence and asserts a zero delta at two levels: the injected source, and the URL loading system under the APIClient a regression might reach for instead. It pumps the run loop before asserting, since every route to a fetch is async, and a second test proves the sequence really did build the glance and rewrite it in place — otherwise deleting the block would leave the zero-request assertion green. Verified falsifiable: adding a glanceActivity fetch to menuWillOpen fails it. Related # Task: T033, T034
The prefix check already covers the exact title, and the pair fit on one over-long line. Related # Task: T033
…telist The projection's semantics were only described in code comments and a swagger annotation, so a client author reading the feature doc had no way to learn that exclude_payloads keeps intent.reason, decision, reason and client identity while dropping everything else. Spell out the kept keys, why has_sensitive_data and request_id survive, and where to go for the full record. Also record the T035 gate results, regenerate ROADMAP.md for the completed task list, and correct quickstart.md, which told the reader to run ./mcpproxy without ever building it. Related #
The core serialises an absent legacy LastActivity as the Go zero time, "0001-01-01T00:00:00Z". That parses, so the start_time fallback never fired and the client was silently dropped for being 2000 years old. Timestamps at or before a year-2000 floor now count as missing.
The minted id was UnixNano plus server and tool, so two calls blocked at the same instant on the same tool shared an id — and the tray, which collapses activity rows by request id, folded two attempts into one row. A process-wide atomic counter is now appended; the existing prefix is kept intact for log greppability.
The projection copied whitelisted keys regardless of value type, so a record whose metadata.reason or intent.reason held an object or array leaked that whole nested payload through the exclude_payloads boundary. Non-string values are now dropped, and an intent left with nothing is dropped with them.
The race test only fails when two mints genuinely land in the same nanosecond, which is a property of the machine rather than of the code — it can pass on a build with no disambiguator at all. Assert the numeric final component advances on every mint, and that one counter is shared across targets.
Fixing the shared helper left twelve call sites formatting their own ids from the clock — internal tool requests (search_servers, retrieve_tools, describe_tool, …), the call_tool dispatch id, and three ToolCallRecord primary keys. Identical concurrent calls therefore still collided, and the tray's collapse-by-request-id undercounted them. mintCorrelationIDAt is now the only place in the package that turns the clock into an identifier; a go/ast test enforces that so a new call site cannot reintroduce the defect. Ids arriving from the transport context are untouched.
The table read last_activity directly, so the core's zero-time sentinel blocked the start_time fallback: rows rendered as "739997d ago" and sank to the bottom of a list sorted by recency. It now asks GlancePresence.lastActivity, which the tray already uses, so the two surfaces cannot drift apart again. The dedupe, sort and age buckets move to DashboardSessions so they can be tested without a view; the sort key is a Date rather than a raw string.
Deploying mcpproxy-docs with
|
| Latest commit: |
2798d98
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://a8810ca0.mcpproxy-docs.pages.dev |
| Branch Preview URL: | https://090-tray-glance-v2.mcpproxy-docs.pages.dev |
|
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
📦 Build ArtifactsWorkflow Run: View Run Available Artifacts
How to DownloadOption 1: GitHub Web UI (easiest)
Option 2: GitHub CLI gh run download 30681847505 --repo smart-mcp-proxy/mcpproxy-go
|
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.
What
Iterates on the tray glance section (PR #930) based on field feedback backed by a 6-week real-usage activity export (3,582 events):
server:toolcalls collapse into one×Nrow (export evidence: 1,479 calls → 809 runs, bursts up to 100×). Failure anywhere in a run dominates its status.intent.reason(98.7% coverage, median 51 chars) renders as the row's subtitle on macOS 14.4+ (single-line + tooltip below), live via SSE too.Backend deltas (additive, back-compat):
exclude_payloadskeeps a contextual-metadata whitelist (string-valued only); policy decisions carryrequest_idend-to-end (all local id mints now share a collision-free counter, incl.ToolCallRecord.IDkeys);GET /api/v1/sessionsorders bylast_activitybefore truncation.Spec & reviews
Full speckit artifacts in
specs/090-tray-glance-v2/(spec, plan, research, data model, contracts, tasks — spec/plan/tasks each Codex-reviewed to APPROVE; implementation Codex-reviewed 3 rounds to APPROVE).Testing
menuWillOpenpath (spec 048 invariant)../internal/...green;make swagger-verifygreen; linter 0 issues.specs/090-tray-glance-v2/verification/manual-protocol.md) is NOT RUN — needs a live app + seeded core.Follow-ups (deliberately deferred)
internal/runtime/runtime.go:1233replay-path record id still uses the raw mint (package-direction refactor; no activity correlation impact).NSMenuItem.subtitlethere) — documented degradation, deployment floor unchanged.