From 7c105957e7c2dbf6b7425360d94e2990fdf5e126 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 25 Oct 2026 09:07:01 -0400 Subject: [PATCH 01/12] fix(hub): the resume path de-chunks only chunked bodies, across read boundaries 83cd8afc made the resume path de-chunk the engine dial unconditionally and discard each read's unconsumed tail. Two consequences: - an identity-encoded body (the frontdoor suite's HTTP/1.0 fakes) was fed to the chunk parser, which consumed nothing, so every gap/live frame was dropped. 4 TestSourceSideSequencing cases failed deterministically since 83cd8afc, and deploy-gate.sh has been red on main. - for real chunked bodies, a chunk-size line or a chunk-closing CRLF split across reads was dropped/misparsed, wedging that member's stream. De-chunk only when the dial head says Transfer-Encoding: chunked, carry the pending tail into the next read, and consume the chunk-closing CRLF in its own state. New TestResumeDechunk feeds a chunked body at every read size. Full frontdoor suite: 64/65 (test_stalled_member_drop... is an order-dependent flake, 3/3 alone). --- ops/hub/hub-frontdoor.py | 30 ++++++++++++++++++++----- ops/hub/test_frontdoor.py | 47 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/ops/hub/hub-frontdoor.py b/ops/hub/hub-frontdoor.py index 7fb01512c..440a00ea4 100644 --- a/ops/hub/hub-frontdoor.py +++ b/ops/hub/hub-frontdoor.py @@ -1606,10 +1606,17 @@ def _dechunk(buf, state): buf = buf[take:] state["size"] -= take if state["size"] == 0: - state["mode"] = "size" - if buf.startswith(b"\r\n"): buf = buf[2:] + # the CRLF closing a chunk can straddle reads: consume it + # in its own state, never as an (empty → unparseable) + # size line — that wedged the stream for good + state["mode"] = "crlf" continue break + if state["mode"] == "crlf": + if len(buf) < 2: break + if buf.startswith(b"\r\n"): buf = buf[2:] + state["mode"] = "size" + continue if state["mode"] == "done": break return out, buf @@ -1649,8 +1656,18 @@ def _resume_loop(self, c, shard_id, addr, cursor): # Byte-level edits (gap frames, synthetic heartbeats, swallowed # frames) are only valid on a stream we fully own. _head, _, _body = pre.partition(b"\r\n\r\n") - _dcst = {"mode": "size", "size": 0} - _clean, _rest = self._dechunk(_body, _dcst) + # de-chunk ONLY a chunked body (an identity-encoded engine body — + # HTTP/1.0, or a future non-chunked transport — parses as-is; the + # unconditional de-chunk dropped every frame of such a body). The + # unconsumed tail (a chunk-size line split across reads) carries + # into the next read instead of being discarded. + _dcst = {"mode": "size", "size": 0, + "chunked": b"transfer-encoding: chunked" in _head.lower()} + _pend = b"" + if _dcst["chunked"]: + _clean, _pend = self._dechunk(_body, _dcst) + else: + _clean = _body reasm += _clean frames, reasm = _parse_frames(reasm) with self.lock: @@ -1725,7 +1742,10 @@ def _resume_loop(self, c, shard_id, addr, cursor): if chunk is None: continue _hb_ticks = 0 - _dc_out, _rest = self._dechunk(chunk, _dcst) + if _dcst["chunked"]: + _dc_out, _pend = self._dechunk(_pend + chunk, _dcst) + else: + _dc_out = chunk reasm += _dc_out frames, reasm = _parse_frames(reasm) if b"server.cursor.unservable" in reasm or any(b"server.cursor.unservable" in f for f in frames): diff --git a/ops/hub/test_frontdoor.py b/ops/hub/test_frontdoor.py index 2b32b95f7..be4a6cdd2 100644 --- a/ops/hub/test_frontdoor.py +++ b/ops/hub/test_frontdoor.py @@ -2345,5 +2345,52 @@ def test_stalled_member_drop_does_not_wedge_the_group(self): "the surviving member must keep receiving (no wedged lock)") + +class TestResumeDechunk(unittest.TestCase): + """The resume path de-chunks the engine's chunked SSE body across + arbitrary read boundaries (83cd8afc follow-up). Group._dechunk is pulled + from the module source by AST — importing the module starts a server.""" + + @classmethod + def setUpClass(cls): + import ast, textwrap + src = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "hub-frontdoor.py")).read() + for node in ast.walk(ast.parse(src)): + if isinstance(node, ast.FunctionDef) and node.name == "_dechunk" and node.args.args and node.args.args[0].arg == "buf": + ns = {} + exec(textwrap.dedent(ast.get_source_segment(src, node)), ns) + cls.dechunk = staticmethod(ns["_dechunk"]) + return + raise AssertionError("Group._dechunk not found") + + @staticmethod + def chunked(*payloads): + return b"".join(b"%x\r\n%s\r\n" % (len(p), p) for p in payloads) + + def feed(self, wire, cut): + st = {"mode": "size", "size": 0} + out, pend = b"", b"" + for i in range(0, len(wire), cut): + o, pend = self.dechunk(pend + wire[i:i + cut], st) + out += o + return out, pend + + def test_every_read_boundary_yields_the_exact_body(self): + frames = [b"id: 1\ndata: {\"a\":1}\n\n", b"id: 2\ndata: " + b"x" * 300 + b"\n\n", b": hb\n\n"] + wire = self.chunked(*frames) + for cut in range(1, len(wire) + 1): + out, _ = self.feed(wire, cut) + self.assertEqual(out, b"".join(frames), "read size %d" % cut) + + def test_a_split_size_line_is_carried_not_dropped(self): + wire = self.chunked(b"data: one\n\n", b"data: two\n\n") + cut = wire.index(b"\r\n", 5) + 3 # inside the second chunk's size line + st = {"mode": "size", "size": 0} + a, pend = self.dechunk(wire[:cut], st) + self.assertEqual(a, b"data: one\n\n") + self.assertTrue(pend, "the partial size line must come back as the pending tail") + b, _ = self.dechunk(pend + wire[cut:], st) + self.assertEqual(b, b"data: two\n\n") + if __name__ == "__main__": unittest.main(verbosity=2) From 957157bace64b1d9ea443f7450d4e61de55ab4d6 Mon Sep 17 00:00:00 2001 From: JJ Lee Date: Mon, 21 Sep 2026 13:16:35 -0400 Subject: [PATCH 02/12] fix(llm): prevent Bedrock throttling errors from being misclassified as context overflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bedrock ThrottlingException messages (e.g. 'Token throughput limit exceeded', 'Too many tokens per minute') match the broad regex patterns in isContextOverflow (specifically /token limit exceeded/i, /too many tokens/i, /exceeds the limit of/i) and are misclassified as context-window overflow. This triggers the compaction recovery path, which also fails (same throttle), producing a misleading 'Conversation history too large to compact' error. The misclassification is amplified by the Bedrock AI SDK adapter's known quirk of coercing undefined to the string 'undefined' — the message() function in provider/error.ts correctly extracts the real error from responseBody, but strips the SDK's '429 Too Many Requests' prefix that would have triggered the 'too many requests' exclusion. Three fixes: 1. Status code guard in parseAPICallError: HTTP 429 and 5xx are never context overflow regardless of message text. This is the highest-leverage fix — every Bedrock throttling error arriving as HTTP 429 is now immune. 2. Tighter isContextOverflow patterns + broader exclusions: - /exceeds the limit of/ now requires a context/input/prompt noun prefix - New exclusions for 'throughput', 'per minute/second/hour/day', 'invocation.*limit/quota', and 'quota' 3. Tests for Bedrock throttling messages that must NOT be classified as context overflow, covering both the pattern-level check and the full APICallError parsing path (including the 'undefined' message workaround). (cherry picked from commit 4e7a41fada8616341ece22610a49c3a0fe545a14) --- .../packages/llm/src/provider-error.ts | 55 ++++++++++++++++++ .../packages/llm/test/provider-error.test.ts | 56 +++++++++++++++++++ .../packages/opencode/src/provider/error.ts | 10 +++- .../opencode/test/session/message-v2.test.ts | 50 +++++++++++++++++ 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 packages/app-bundle/overlay/packages/llm/src/provider-error.ts create mode 100644 packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts diff --git a/packages/app-bundle/overlay/packages/llm/src/provider-error.ts b/packages/app-bundle/overlay/packages/llm/src/provider-error.ts new file mode 100644 index 000000000..f20017834 --- /dev/null +++ b/packages/app-bundle/overlay/packages/llm/src/provider-error.ts @@ -0,0 +1,55 @@ +import { Schema } from "effect" +import { LLMError, ProviderErrorEvent } from "./schema" + +const patterns = [ + /prompt is too long/i, + /request_too_large/i, + /input is too long for requested model/i, + /exceeds the context window/i, + /exceeds (?:the )?(?:model'?s )?maximum context length(?: of [\d,]+ tokens?|\s*\([\d,]+\))/i, + /input token count.*exceeds the maximum/i, + /tokens in request more than max tokens allowed/i, + /maximum prompt length is \d+/i, + /reduce the length of the messages/i, + /maximum context length is \d+ tokens/i, + /exceeds (?:the )?maximum allowed input length of [\d,]+ tokens?/i, + /input \(\d+ tokens\) is longer than the model'?s context length \(\d+ tokens\)/i, + // Tightened: require a context/input/prompt noun so throughput/quota limits + // like "exceeds the limit of 100000 tokens per minute" don't false-positive. + /(?:input|context|prompt|message).*exceeds the limit of \d+/i, + /exceeds the available context size/i, + /greater than the context length/i, + /context window exceeds limit/i, + /exceeded model token limit/i, + /context[_ ]length[_ ]exceeded/i, + /request entity too large/i, + /context length is only \d+ tokens/i, + /input length.*exceeds.*context length/i, + /prompt too long; exceeded (?:max )?context length/i, + /too large for model with \d+ maximum context length/i, + /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, + /model_context_window_exceeded/i, + /too many tokens/i, + /token limit exceeded/i, +] + +const exclusions = [ + /^(throttling error|service unavailable):/i, + /rate limit/i, + /too many requests/i, + // Bedrock ThrottlingException messages that match broad patterns above but + // are throughput/quota limits, not context-window overflow. + /throughput/i, + /per\s+(minute|second|hour|day)/i, + /invocation.*(limit|quota|exceeded)/i, + /quota/i, +] + +export const isContextOverflow = (message: string) => + !exclusions.some((pattern) => pattern.test(message)) && + (patterns.some((pattern) => pattern.test(message)) || /^4(00|13)\s*(status code)?\s*\(no body\)/i.test(message)) + +export const isContextOverflowFailure = (failure: unknown) => + failure instanceof LLMError + ? failure.reason._tag === "InvalidRequest" && failure.reason.classification === "context-overflow" + : Schema.is(ProviderErrorEvent)(failure) && failure.classification === "context-overflow" diff --git a/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts b/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts new file mode 100644 index 000000000..123bfaeb9 --- /dev/null +++ b/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "bun:test" +import { isContextOverflow } from "../src" + +describe("provider error classification", () => { + test("classifies provider token limit messages as context overflow", () => { + const messages = [ + "tokens in request more than max tokens allowed", + '{"error":{"type":"request_too_large","message":"Request exceeds the maximum size"}}', + "Requested token count exceeds the model's maximum context length of 131072 tokens.", + "Input length (265330) exceeds model's maximum context length (262144).", + "Input length 131393 exceeds the maximum allowed input length of 131040 tokens.", + "The input (516368 tokens) is longer than the model's context length (262144 tokens).", + "Prompt has 5,958,968 tokens, but the configured context size is 256,000 tokens", + "Too many tokens", + "Token limit exceeded", + ] + + expect(messages.every(isContextOverflow)).toBe(true) + }) + + test("does not classify rate limits as context overflow", () => { + const messages = [ + "Throttling error: Too many tokens, please wait before trying again.", + "Rate limit exceeded, please retry after 30 seconds.", + "Too many requests. Please slow down.", + ] + + expect(messages.some(isContextOverflow)).toBe(false) + }) + + test("does not classify Bedrock throttling/quota messages as context overflow", () => { + const messages = [ + // ThrottlingException messages that match broad patterns (too many tokens, + // token limit exceeded, exceeds the limit) but are throughput limits. + "Token throughput limit exceeded", + "Too many tokens per minute for model anthropic.claude-opus-4-8-20250808-v1:0", + "You have exceeded the model invocation throughput limit of 100000 tokens per minute", + "Model invocation throughput limit exceeded", + "Insufficient quota for this request", + "Request quota exceeded, please try again later", + "Token throughput quota exceeded: too many tokens per second", + ] + + expect(messages.some(isContextOverflow)).toBe(false) + }) + + test("still classifies real Bedrock context overflow as context overflow", () => { + const messages = [ + "Input is too long for requested model. Max input token count: 200000, actual token count: 250000", + "Input is too long for requested model", + "Input length 265330 exceeds the context window of 200000 tokens", + ] + + expect(messages.every(isContextOverflow)).toBe(true) + }) +}) diff --git a/packages/app-bundle/overlay/packages/opencode/src/provider/error.ts b/packages/app-bundle/overlay/packages/opencode/src/provider/error.ts index 4c72e3750..592590277 100644 --- a/packages/app-bundle/overlay/packages/opencode/src/provider/error.ts +++ b/packages/app-bundle/overlay/packages/opencode/src/provider/error.ts @@ -176,7 +176,15 @@ export type ParsedAPICallError = export function parseAPICallError(input: { providerID: ProviderV2.ID; error: APICallError }): ParsedAPICallError { const m = message(input.providerID, input.error) const body = json(input.error.responseBody) - if (isContextOverflow(m) || input.error.statusCode === 413 || body?.error?.code === "context_length_exceeded") { + // Status-code guard: 429 (rate limit) and 5xx (server error) are never + // context overflow, even when the error message text happens to match the + // heuristic patterns (e.g. Bedrock ThrottlingException "token throughput + // limit exceeded" matches /token limit exceeded/i but is a rate limit). + // The `undefined` status case (stream errors without HTTP metadata) still + // runs the heuristic — only known non-overflow statuses are excluded. + const status = input.error.statusCode + const overflowEligible = status === undefined || (status >= 400 && status < 500 && status !== 429) + if (overflowEligible && (isContextOverflow(m) || status === 413 || body?.error?.code === "context_length_exceeded")) { return { type: "context_overflow", message: m, diff --git a/packages/app-bundle/overlay/packages/opencode/test/session/message-v2.test.ts b/packages/app-bundle/overlay/packages/opencode/test/session/message-v2.test.ts index 66020d81e..0da894814 100644 --- a/packages/app-bundle/overlay/packages/opencode/test/session/message-v2.test.ts +++ b/packages/app-bundle/overlay/packages/opencode/test/session/message-v2.test.ts @@ -1706,6 +1706,56 @@ describe("session.message-v2.fromError", () => { expect(SessionV1.APIError.isInstance(result)).toBe(true) }) + test("does not classify Bedrock 429 throttling with overflow-matching body as context overflow", () => { + // Bedrock's @ai-sdk/amazon-bedrock adapter coerces undefined to "undefined", + // stripping the SDK's "429 Too Many Requests" prefix that would otherwise + // trigger the "too many requests" exclusion in isContextOverflow. The status + // code guard in parseAPICallError must prevent the raw Bedrock message + // (which matches /token limit exceeded/i) from being misclassified. + const bedrockMessages = [ + "Token throughput limit exceeded", + "Too many tokens per minute for model anthropic.claude-opus-4-8-20250808-v1:0", + "You have exceeded the model invocation throughput limit of 100000 tokens per minute", + ] + + for (const bodyMessage of bedrockMessages) { + const result = MessageV2.fromError( + new APICallError({ + message: "undefined", + url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-opus-4-8-20250808-v1%3A0/converse-stream", + requestBodyValues: {}, + statusCode: 429, + responseHeaders: { "content-type": "application/json" }, + responseBody: JSON.stringify({ message: bodyMessage }), + isRetryable: true, + }), + { providerID: ProviderV2.ID.make("amazon-bedrock") }, + ) + expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false) + expect(SessionV1.APIError.isInstance(result)).toBe(true) + } + }) + + test("does not classify Bedrock 400 throttling with overflow-matching body as context overflow", () => { + // Bedrock ValidationException (HTTP 400) can carry throughput messages that + // match context-overflow patterns. The broadened exclusions in + // isContextOverflow must prevent these from being misclassified. + const result = MessageV2.fromError( + new APICallError({ + message: "undefined", + url: "https://bedrock-runtime.us-east-1.amazonaws.com/model/anthropic.claude-opus-4-8-20250808-v1%3A0/converse-stream", + requestBodyValues: {}, + statusCode: 400, + responseHeaders: { "content-type": "application/json" }, + responseBody: JSON.stringify({ message: "Token throughput limit exceeded" }), + isRetryable: false, + }), + { providerID: ProviderV2.ID.make("amazon-bedrock") }, + ) + expect(SessionV1.ContextOverflowError.isInstance(result)).toBe(false) + expect(SessionV1.APIError.isInstance(result)).toBe(true) + }) + test("serializes unknown inputs", () => { const result = MessageV2.fromError(123, { providerID }) From d03408f8c2e0dfbc7ef72c58a1bd83b726bc5607 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Mon, 21 Sep 2026 17:33:26 +0000 Subject: [PATCH 03/12] fix(llm): require exact matches for generic token overflow errors to avoid misclassifying quota limits (cherry picked from commit 34e910a09272d18927ddcf5764f3113030081fd5) --- .../app-bundle/overlay/packages/llm/src/provider-error.ts | 4 ++-- .../overlay/packages/llm/test/provider-error.test.ts | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/app-bundle/overlay/packages/llm/src/provider-error.ts b/packages/app-bundle/overlay/packages/llm/src/provider-error.ts index f20017834..29c81cbc2 100644 --- a/packages/app-bundle/overlay/packages/llm/src/provider-error.ts +++ b/packages/app-bundle/overlay/packages/llm/src/provider-error.ts @@ -29,8 +29,8 @@ const patterns = [ /too large for model with \d+ maximum context length/i, /prompt has [\d,]+ tokens?, but the configured context size is [\d,]+ tokens?/i, /model_context_window_exceeded/i, - /too many tokens/i, - /token limit exceeded/i, + /^too many tokens$/i, + /^token limit exceeded$/i, ] const exclusions = [ diff --git a/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts b/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts index 123bfaeb9..13066525a 100644 --- a/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts +++ b/packages/app-bundle/overlay/packages/llm/test/provider-error.test.ts @@ -23,6 +23,7 @@ describe("provider error classification", () => { "Throttling error: Too many tokens, please wait before trying again.", "Rate limit exceeded, please retry after 30 seconds.", "Too many requests. Please slow down.", + "Monthly token limit exceeded", ] expect(messages.some(isContextOverflow)).toBe(false) From cc0f0ab3e1cb32a755d5a46790d5407c58ecae4e Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 25 Oct 2026 09:23:08 -0400 Subject: [PATCH 04/12] chore(app-bundle): manifest refresh for the #1386 cherry-pick --- packages/app-bundle/manifest.json | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 542167f12..4926f1353 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -52,7 +52,9 @@ "packages/core/test/session-runner.test.ts": "4cf0d4e3989a32b0c693473d4f94bf787bd964e79b312a7ee0032305503edb20", "packages/core/test/tool-question.test.ts": "4df41393f15104a1d3a9c3d06e30cef515108490b41fcf4d9a7d9399fd4ed945", "packages/llm/src/cache-policy.ts": "04c1fb5d3d061cfa95d04e54df0a1c2cdaafac921224242446eaa2968bff7857", + "packages/llm/src/provider-error.ts": "65b9124905f0755c1e368df5150aa211bea3cc29afb66845b5ceaccba10042dd", "packages/llm/test/cache-policy.test.ts": "e921095c4301440c786105063439e49b2fedbaf6d46f30457e0a96388d268f4b", + "packages/llm/test/provider-error.test.ts": "ac3bf099319acdc27ff47ea79fd74357d55e12ab95131a07a72a5a2a9bc0c08f", "packages/opencode/script/build.ts": "876e7d569aa71ce060c1d4b24c96e9bf13245cbf4ca743828d645560a95aa641", "packages/opencode/script/httpapi-exercise.ts": "772bd2cf5748ca90c5d5790c2d044237518cc8ede0411fc4ba28b9d7440ba28c", "packages/opencode/test/amicode-release-workflow.test.ts": "f6dfcb3ed9c572ca8e8b21ff82e038872b635b25c68f9580f6d6f51ee2813189", @@ -355,7 +357,7 @@ "packages/opencode/src/project/instance-context.ts": "65a35792e50e96a50ea14cd9f928045c0e2fc15eb49506f5bbceee38c4af1457", "packages/opencode/src/project/instance-store.ts": "c22d3e2499e63dd8d9395d58189df75444d306774df833e746f22e327edcf2ab", "packages/opencode/src/provider/auth.ts": "b0ab1322c1039e27176bf9e3500603eae27506065ac141a69710220f3361f6b8", - "packages/opencode/src/provider/error.ts": "7f3c8210c775e2ce98140bf015dc97e61192912af062c75e18ecb09ee2736a0f", + "packages/opencode/src/provider/error.ts": "d72310be26cfae5c2943f9939a6461e848532a5646ff405d628e9a50fc1ebe91", "packages/opencode/src/provider/provider.ts": "b89bc0ca0a12ea0cfaf5c956a4b731a0397d4c407e787a16cdd66fb7f54ee476", "packages/opencode/src/provider/transform.ts": "70c7492d4a23a30ebc340a5605702b1602960f4b412e18557534e02962c137b8", "packages/opencode/src/question/index.ts": "8317348c2ca27dd9e4f7d18fdc126a4d355c20495c1b86668430ad477a68e9b3", @@ -455,7 +457,7 @@ "packages/opencode/test/session/business-record.test.ts": "7857094479408bcfd6ea8d9f5b425c6384bcf8490f930674bc1c203e82782d75", "packages/opencode/test/session/instruction.test.ts": "06e1b371a08c7d07ed7688b997eb43ca9957ccd07936f601797d7f04a2bf0ab7", "packages/opencode/test/session/llm-native-recorded.test.ts": "7139b58569af0172862af521e8f8d7a78132f40f06dcceb4baf8ca3a2f07d3bd", - "packages/opencode/test/session/message-v2.test.ts": "3a45b7b884a33ee48daf533ac84bf5c0b34d01845b8c1c4cc7e68de88b063ed1", + "packages/opencode/test/session/message-v2.test.ts": "90fbd916cea08ddb1de066dccf2677490b547afc5805aa36e55683b28fbb079f", "packages/opencode/test/session/mutation.test.ts": "ccb82b6858ac097d2af09a94497d05f6a0ff843b928a69e763fdb510905a10cd", "packages/opencode/test/session/processor-effect.test.ts": "51ebe1cb9b7b0e1d249472367491a1306b5add24ca8b04d5dc15b2b2fa1f1b56", "packages/opencode/test/session/prompt.test.ts": "e3d0748edb833640ef284857ae725e4b974843480d4128f56239e3fd1e6d481f", @@ -1013,7 +1015,7 @@ "packages/ui/src/v2/components/text-shimmer-v2.tsx" ], "exceptions": [], - "extracted_at": "2026-10-23T13:24:11.640Z", + "extracted_at": "2026-10-25T13:23:01.971Z", "per_package": { "packages/app": { "A": 254, @@ -1027,7 +1029,7 @@ }, "packages/llm": { "A": 1, - "M": 5, + "M": 7, "D": 0 }, "packages/opencode": { @@ -1062,7 +1064,7 @@ } }, "counts": { - "overlay_total": 991, + "overlay_total": 993, "deletions": 7, "server_coupled": 0 }, @@ -1111,7 +1113,9 @@ "packages/core/test/session-runner.test.ts": "M", "packages/core/test/tool-question.test.ts": "M", "packages/llm/src/cache-policy.ts": "M", + "packages/llm/src/provider-error.ts": "M", "packages/llm/test/cache-policy.test.ts": "M", + "packages/llm/test/provider-error.test.ts": "M", "packages/opencode/script/build.ts": "M", "packages/opencode/script/httpapi-exercise.ts": "M", "packages/opencode/test/amicode-release-workflow.test.ts": "A", From c4909966fb845c83430a08b3a1d046155b337fd1 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 25 Oct 2026 09:26:48 -0400 Subject: [PATCH 05/12] fix(engine): drop the heartbeat debug console.log from stdout; pin route handlers off stdout (#1834) --- packages/app-bundle/manifest.json | 10 ++++--- .../instance/httpapi/handlers/global.ts | 1 - .../test/server/stdout-channel.test.ts | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) create mode 100644 packages/app-bundle/overlay/packages/opencode/test/server/stdout-channel.test.ts diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 4926f1353..3bc9639ca 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -450,6 +450,7 @@ "packages/opencode/test/server/session-diff-missing-patch.test.ts": "70526ca7fb86741843fd167f8f2f9c6f6d7e5e415a20a5448d17d24229bdd65b", "packages/opencode/test/server/session-diff-scoped.test.ts": "20ef80b92ad872573b371540575f028be8f1ad3c9f34d72f814b621e14e7a59e", "packages/opencode/test/server/sse-tick-telemetry.test.ts": "95738379df27d2da236f968a3d197cf27bcce17e8ec8ce1d288d4d2c1184d8ee", + "packages/opencode/test/server/stdout-channel.test.ts": "812080bb29d09008dacf29741e8f6f06b4ac9fe4623e6992a046313597c3dd1c", "packages/opencode/test/server/summary-diff-wire.test.ts": "080236ce0a7985195a8f385ae65311d63dc18826439a9729c7c87ce4f55efd55", "packages/opencode/test/server/tcp-keepalive.test.ts": "e5c26de10fe44fd6485e0f75f667b2662f69896135f014b0b86772a6ca48fff5", "packages/opencode/test/server/workspace-routing.test.ts": "2574731ac379c540f450edc15f8750ac5360e8c33b4a0dad7c61e8eb686137bb", @@ -994,7 +995,7 @@ "packages/opencode/src/server/routes/instance/httpapi/handlers/event.ts": "88c7bae32fa01ab213e95cb4e4891fd3ccb961782e80a416e321c5daf90fb487", "packages/opencode/src/server/routes/instance/httpapi/handlers/experimental.ts": "69ca3dd48d95157fc822e1f76eb42ac873df50f22616b85194619ca658613867", "packages/opencode/src/server/routes/instance/httpapi/handlers/file.ts": "10bcb79b117beebee04184fb48c1ad789e782e2f7ae4b0c68603edc00f03aee9", - "packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts": "a31e4d0abe4f76e667167c5017aca5559b53938db63bdc1d656a154e1aaf5fea", + "packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts": "040554475b6ba22ad666dfeaca2576658854c46cd18efe3b2d68959f9eaaed80", "packages/opencode/src/server/routes/instance/httpapi/handlers/mcp.ts": "7da08d58765cdf2b6ce4648fc70de371402949f5bb7b75696f674d43f2d5365b", "packages/opencode/src/server/routes/instance/httpapi/handlers/provider.ts": "8baf16cad0992c5351b61b0c0fb27903bb25b17e1b25fc7c5d77f807c24413a6", "packages/opencode/src/server/routes/instance/httpapi/handlers/session.ts": "b37ff4136c4aea85b23083bf441f03e09820e3e4cdd95e4118ca570d8435b94f", @@ -1015,7 +1016,7 @@ "packages/ui/src/v2/components/text-shimmer-v2.tsx" ], "exceptions": [], - "extracted_at": "2026-10-25T13:23:01.971Z", + "extracted_at": "2026-10-25T13:26:45.953Z", "per_package": { "packages/app": { "A": 254, @@ -1033,7 +1034,7 @@ "D": 0 }, "packages/opencode": { - "A": 98, + "A": 99, "M": 100, "D": 0 }, @@ -1064,7 +1065,7 @@ } }, "counts": { - "overlay_total": 993, + "overlay_total": 994, "deletions": 7, "server_coupled": 0 }, @@ -1511,6 +1512,7 @@ "packages/opencode/test/server/session-diff-missing-patch.test.ts": "M", "packages/opencode/test/server/session-diff-scoped.test.ts": "A", "packages/opencode/test/server/sse-tick-telemetry.test.ts": "A", + "packages/opencode/test/server/stdout-channel.test.ts": "A", "packages/opencode/test/server/summary-diff-wire.test.ts": "A", "packages/opencode/test/server/tcp-keepalive.test.ts": "A", "packages/opencode/test/server/workspace-routing.test.ts": "M", diff --git a/packages/app-bundle/overlay/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts b/packages/app-bundle/overlay/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts index 797bfa741..75dfe16bc 100644 --- a/packages/app-bundle/overlay/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts +++ b/packages/app-bundle/overlay/packages/opencode/src/server/routes/instance/httpapi/handlers/global.ts @@ -131,7 +131,6 @@ function eventResponse() { Stream.drop(1), Stream.mapEffect(() => Effect.map(Clock.currentTimeMillis, (time) => { const seq = GlobalEventReplay.current - console.log("[hb] tick fired, offering beat"); Queue.offerUnsafe(queue, controlEvent(seq, "server.heartbeat", { time, seq })) })), Stream.tap(() => diff --git a/packages/app-bundle/overlay/packages/opencode/test/server/stdout-channel.test.ts b/packages/app-bundle/overlay/packages/opencode/test/server/stdout-channel.test.ts new file mode 100644 index 000000000..a618df23d --- /dev/null +++ b/packages/app-bundle/overlay/packages/opencode/test/server/stdout-channel.test.ts @@ -0,0 +1,26 @@ +import { expect, test } from "bun:test" +import { readdirSync, readFileSync, statSync } from "node:fs" +import path from "node:path" + +// #1834: stdout is the CLI data channel (`opencode run --format json`). +// Request/stream handlers run inside CLI processes too, so they must never +// write to stdout — engine telemetry goes to stderr / the logger. +test("server route handlers never write to stdout", () => { + const root = path.join(import.meta.dir, "../../src/server/routes") + const offenders: string[] = [] + const walk = (dir: string) => { + for (const name of readdirSync(dir)) { + const file = path.join(dir, name) + if (statSync(file).isDirectory()) walk(file) + else if (/\.tsx?$/.test(name)) { + readFileSync(file, "utf8") + .split("\n") + .forEach((line, i) => { + if (/\bconsole\.(log|info)\(/.test(line)) offenders.push(`${path.relative(root, file)}:${i + 1}`) + }) + } + } + } + walk(root) + expect(offenders).toEqual([]) +}) From 3a615bf6256604db0eeea08410c3b01e59d1a1a2 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 25 Oct 2026 09:28:30 -0400 Subject: [PATCH 06/12] =?UTF-8?q?fix(extension):=20file-link=20clicks=20ne?= =?UTF-8?q?ver=20die=20silently=20=E2=80=94=20name=20the=20no-context=20an?= =?UTF-8?q?d=20stale-path=20refusals;=20a=20throwing=20context=20callback?= =?UTF-8?q?=20cannot=20swallow=20the=20click=20(#1845)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/extension/src/chat_bridge.ts | 18 +++++++++++++-- packages/extension/src/chat_panel.ts | 2 +- packages/extension/src/deck_panel.ts | 2 +- .../test/link_resolution_1826.test.ts | 22 +++++++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/packages/extension/src/chat_bridge.ts b/packages/extension/src/chat_bridge.ts index 90270a00b..1fd62a3e5 100644 --- a/packages/extension/src/chat_bridge.ts +++ b/packages/extension/src/chat_bridge.ts @@ -454,7 +454,15 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean if (!path.isAbsolute(fsPath) || fsPath.length > 4096) return true; const linkResolution = io.linkResolution; if (!linkResolution) { - if (!fs.existsSync(fsPath)) return true; + // #1845: never a silent no-op — without the posture context a path + // that isn't on this disk names itself (on a hub-backed panel the path + // lives on the hub, which is the clue that the context never wired). + if (!fs.existsSync(fsPath)) { + vscode.window.showWarningMessage( + `Amicode: link not opened — ${fsPath} is not on this machine (no link-resolution context for this panel; reload the window if it is a hub session)`, + ); + return true; + } // Route to the Preview companion tab (#935) — the session page's // preview-file listener (from #934) sets previewFile, opens the side // panel, and switches to the Preview tab. @@ -477,8 +485,14 @@ export function handleAmicodeBridgeMessage(msg: unknown, io: BridgeIo): boolean }); } else if (resolution.action === "failed") { vscode.window.showErrorMessage(`Amicode: link not opened — ${resolution.reason} (${resolution.detail})`); + } else { + // #1845: the non-hub postures' refusal (stale path) is named too. + vscode.window.showWarningMessage( + `Amicode: link not opened — ${fsPath} is not on this machine (posture: ${linkResolution.posture})`, + ); } - // "silent": the non-hub posture's unchanged refusal. + }, (err: unknown) => { + vscode.window.showErrorMessage(`Amicode: link not opened — ${err instanceof Error ? err.message : String(err)}`); }); return true; } diff --git a/packages/extension/src/chat_panel.ts b/packages/extension/src/chat_panel.ts index 530bc0371..89fc92dfb 100644 --- a/packages/extension/src/chat_panel.ts +++ b/packages/extension/src/chat_panel.ts @@ -249,7 +249,7 @@ export class ChatPanel implements LaunchTarget { // #1551: the enable-control act → the host's modal + self-owned mint. onFleetEnableControl: ChatPanel.onFleetEnableControlCallback, previewVisibleChildren: ChatPanel.previewVisibleChildrenCallback, - linkResolution: ChatPanel.linkResolutionContextCallback?.(), + linkResolution: (() => { try { return ChatPanel.linkResolutionContextCallback?.(); } catch (err) { console.error("[amicode] link-resolution context failed", err); return undefined; } })(), explorerIconTheme: resolveExplorerIconTheme, }); if (!handled) console.log("[amicode/chat] webview msg:", msg); diff --git a/packages/extension/src/deck_panel.ts b/packages/extension/src/deck_panel.ts index 1cba6f9c6..0d1e255b1 100644 --- a/packages/extension/src/deck_panel.ts +++ b/packages/extension/src/deck_panel.ts @@ -60,7 +60,7 @@ export class DeckPanel { visible: () => this.panel.visible, postToWebview: (m) => void this.panel.webview.postMessage(m), ...(serverAuth ? { server: { url: serverUrl, authorization: serverAuth } } : {}), - linkResolution: DeckPanel.linkResolutionContextCallback?.(), + linkResolution: (() => { try { return DeckPanel.linkResolutionContextCallback?.(); } catch (err) { console.error("[amicode] link-resolution context failed", err); return undefined; } })(), // Bug-session lifecycle (#250) — deck panes never carry the // amicode_bug_report boot param, so no dock lives here; wired for // uniformity (the manager drops unknown ids anyway). diff --git a/packages/extension/test/link_resolution_1826.test.ts b/packages/extension/test/link_resolution_1826.test.ts index d5d0c3f37..bd0af0d1f 100644 --- a/packages/extension/test/link_resolution_1826.test.ts +++ b/packages/extension/test/link_resolution_1826.test.ts @@ -257,3 +257,25 @@ describe("the remote affordance — vscode-remote explicit (#1826)", () => { expect(window_.messages[0]).toContain("no-ssh-alias"); }); }); + +// #1845: the open-file handler never dies silently — the last two silent +// branches (no posture context; the non-hub posture's stale-path refusal) +// each name themselves with a toast. +describe("no silent link clicks (#1845)", () => { + const missing = "/home/aaron/harmoniqs/definitely-not-on-this-disk-1845/notes.md"; + + it("without a link-resolution context, a path absent locally toasts instead of doing nothing", () => { + const host = io(); + handleAmicodeBridgeMessage({ source: "amicode", kind: "open-file", url: `file://${missing}` }, host); + expect(host.posted).toEqual([]); + expect(window_.messages.some((m) => m.includes("link not opened") && m.includes(missing))).toBe(true); + }); + + it("a non-hub posture's stale path toasts instead of the old silent refusal", async () => { + const host = io({ posture: "fleet-client" }); + handleAmicodeBridgeMessage({ source: "amicode", kind: "open-file", url: `file://${missing}` }, host); + await flush(); + expect(host.posted).toEqual([]); + expect(window_.messages.some((m) => m.includes("link not opened") && m.includes("fleet-client"))).toBe(true); + }); +}); From 015cd58beb54f5e3d275380722c129a7afa53013 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 25 Oct 2026 09:30:28 -0400 Subject: [PATCH 07/12] =?UTF-8?q?fix(app):=20a=20dead=20turn=20retires=20i?= =?UTF-8?q?ts=20question/permission=20cards=20=E2=80=94=20no=20more=20requ?= =?UTF-8?q?est-not-found=20replies=20(#1754)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/app-bundle/manifest.json | 6 +-- .../app/src/context/server-session.test.ts | 47 +++++++++++++++++++ .../app/src/context/server-session.ts | 23 ++++++++- 3 files changed, 71 insertions(+), 5 deletions(-) diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index 3bc9639ca..cfce89fa1 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -202,8 +202,8 @@ "packages/app/src/context/server-sdk.tsx": "9dc721c54074a323fb879aedf76cf29c1888088ab6b9b0c68aae683ca4f563f1", "packages/app/src/context/server-session-child-active.test.ts": "de8c467f67f88b6cce351c1805ec90728cf238eb1ef32f39ffdbbf3de4dcec22", "packages/app/src/context/server-session-orphan-part.test.ts": "1d635374c0bfff25a01825061aabac7334e5cdaedca2dab9f3d9467ff543caea", - "packages/app/src/context/server-session.test.ts": "31b14c85a843eb172899f456566ad7ab7d07a22eaf8b31d29a22e255e95cb96b", - "packages/app/src/context/server-session.ts": "9fbb312e7183ca0f1266ee13b59fb8545a64a8e9539645790c8969e7bdf4053b", + "packages/app/src/context/server-session.test.ts": "b332098635885211bccaec0d7da8ca6b60696b04bd3f89ba3fcd97cd4c881856", + "packages/app/src/context/server-session.ts": "ad486fc5d36f93f5f53d51b70fdf191a3008b67f2b280dfc58b64e6d8d20982d", "packages/app/src/context/server-sync.tsx": "55ac830560ddd947b5d58a9c496a4e94ffc63fd7241c74ba9b843aea724b74ce", "packages/app/src/context/server.tsx": "08e978e4b4b3094221a2a0b92e069f39cbedb6d6cf95935881e21cde52265ee2", "packages/app/src/context/session-mirror.test.ts": "65e6d6474e4e67f2da194047140c8c8aac04fe0cc9926441468e53fabc48db96", @@ -1016,7 +1016,7 @@ "packages/ui/src/v2/components/text-shimmer-v2.tsx" ], "exceptions": [], - "extracted_at": "2026-10-25T13:26:45.953Z", + "extracted_at": "2026-10-25T13:30:07.394Z", "per_package": { "packages/app": { "A": 254, diff --git a/packages/app-bundle/overlay/packages/app/src/context/server-session.test.ts b/packages/app-bundle/overlay/packages/app/src/context/server-session.test.ts index f2d18766e..404986919 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/server-session.test.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/server-session.test.ts @@ -742,3 +742,50 @@ describe("amicode#1787 — reconcile semantics at the chip (busy/idle/omitted)", expect(chipFor(session, "o5")).toBeUndefined() // back to honest omission }) }) + +// #1754: a dead turn's cards retire. A pending question/permission blocks its +// turn, so an authoritative turn end (idle status, session.error) means the +// engine forgot the request — a card left up only ever toasts "not found". +describe("stale question/permission cards retire with their turn (#1754)", () => { + const ask = (session: ReturnType, sid: string) => { + session.apply({ type: "session.created", properties: { info: sessionInfo(sid) } }) + session.apply({ type: "session.status", properties: { sessionID: sid, status: { type: "busy" } } }) + session.apply({ + type: "question.asked", + properties: { id: "que_dead", sessionID: sid, questions: [{ header: "h", question: "q?", options: [] }] }, + }) + session.apply({ + type: "permission.asked", + properties: { id: "per_dead", sessionID: sid, permission: "edit", patterns: ["**"] }, + }) + } + + test("a still-busy turn keeps its cards (retry included)", () => { + const session = createSession() + ask(session, "s_live") + session.apply({ + type: "session.status", + properties: { sessionID: "s_live", status: { type: "retry", attempt: 1, message: "429", next: 0 } }, + }) + expect(session.data.question.s_live?.length).toBe(1) + expect(session.data.permission.s_live?.length).toBe(1) + }) + + test("an idle status retires the turn's question and permission cards", () => { + const session = createSession() + ask(session, "s_idle") + session.apply({ type: "session.status", properties: { sessionID: "s_idle", status: { type: "idle" } } }) + expect(session.data.question.s_idle?.length ?? 0).toBe(0) + expect(session.data.permission.s_idle?.length ?? 0).toBe(0) + }) + + test("session.error retires them too, and other sessions keep theirs", () => { + const session = createSession() + ask(session, "s_err") + ask(session, "s_other") + session.apply({ type: "session.error", properties: { sessionID: "s_err", error: { name: "UnknownError" } } }) + expect(session.data.question.s_err?.length ?? 0).toBe(0) + expect(session.data.permission.s_err?.length ?? 0).toBe(0) + expect(session.data.question.s_other?.length).toBe(1) + }) +}) diff --git a/packages/app-bundle/overlay/packages/app/src/context/server-session.ts b/packages/app-bundle/overlay/packages/app/src/context/server-session.ts index 92336303f..99d545a40 100644 --- a/packages/app-bundle/overlay/packages/app/src/context/server-session.ts +++ b/packages/app-bundle/overlay/packages/app/src/context/server-session.ts @@ -343,6 +343,16 @@ export function createServerSession( turnActive.delete(sessionID) clearTurnFloorTimer(sessionID) } + // #1754: a pending question/permission blocks its turn, so an AUTHORITATIVE + // turn end (the server's idle status, a terminal execution frame, or + // session.error) means every card still up for that session belongs to a + // dead turn — the engine has forgotten the request and any reply would + // toast "request not found". Retire them. Only server frames call this — + // never the local turn-floor timeout (a slow human answer is still live). + const retirePending = (sessionID: string) => { + if (data.question[sessionID]?.length) setData("question", sessionID, []) + if (data.permission[sessionID]?.length) setData("permission", sessionID, []) + } const markTurnActive = (sessionID: string, timeoutMs = TURN_FLOOR_TIMEOUT_MS) => { turnActive.add(sessionID) clearTurnFloorTimer(sessionID) @@ -1402,7 +1412,10 @@ export function createServerSession( if (!stale) { stampStatusMutation(sessionID) setData("session_status", sessionID, reconcile(props.status)) - if (props.status.type === "idle") cancelReconcileTimer(sessionID) + if (props.status.type === "idle") { + cancelReconcileTimer(sessionID) + retirePending(sessionID) + } // #1649 — the ancestor floor: a parent blocked on a foreground // subagent re-derives its child-active count from session_status, // so the parent's rail follows child status edges reactively — @@ -1445,6 +1458,7 @@ export function createServerSession( event.type === "session.execution.interrupted" ) { cancelReconcileTimer(sessionID) + retirePending(sessionID) setData("session_status", sessionID, { type: "idle" }) } if (event.type === "session.retry.scheduled") @@ -1534,7 +1548,10 @@ export function createServerSession( // (The mid-turn floor is held by an active part stream / a fresh execution // bracket — a lone idle frame does not race those; it settles a turn that // has otherwise gone quiet.) - if ((props.status?.type ?? "idle") === "idle") clearTurnActive(props.sessionID) + if ((props.status?.type ?? "idle") === "idle") { + clearTurnActive(props.sessionID) + retirePending(props.sessionID) + } stampStatusMutation(props.sessionID) setData("session_status", props.sessionID, reconcile(props.status)) // #1649 — if this session is a spawned child, propagate its new @@ -1562,6 +1579,7 @@ export function createServerSession( if (!id) return cancelReconcileTimer(id) clearTurnActive(id) + retirePending(id) stampStatusMutation(id) setData("session_status", id, { type: "idle" }) // #1649 — a child turn's terminal lowers its ancestor floor. @@ -1578,6 +1596,7 @@ export function createServerSession( const id = props?.sessionID ?? eventID if (!id) return clearTurnActive(id) + retirePending(id) if ((data.session_status[id]?.type ?? "idle") !== "idle") setData("session_status", id, { type: "idle" }) // #1649 — a child error settles it idle; lower its ancestor floor. refreshChildActive(id) From 583cb63189597dee681da1ba586e202899160385 Mon Sep 17 00:00:00 2001 From: Aaron Trowbridge Date: Sun, 25 Oct 2026 10:25:45 -0400 Subject: [PATCH 08/12] chore(extension): version the live-test lane 0.3.9-lane.1 (above the marketplace and the unpublished 0.3.8) --- .../__pycache__/hub-frontdoor.cpython-310.pyc | Bin 58935 -> 73344 bytes .../__pycache__/jev_placement.cpython-310.pyc | Bin 9507 -> 9502 bytes packages/extension/package.json | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/ops/hub/__pycache__/hub-frontdoor.cpython-310.pyc b/ops/hub/__pycache__/hub-frontdoor.cpython-310.pyc index ec2460759b9600171a0cdc33c51dd382e3779932..2575083213545bd6ae0c20e2dab215e636879e49 100644 GIT binary patch delta 30316 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 diff --git a/ops/hub/__pycache__/jev_placement.cpython-310.pyc b/ops/hub/__pycache__/jev_placement.cpython-310.pyc index abfaa70fd1e2fd7f62d80d265507cb5ae7d5081c..aabdac10f6a99655560f5f15761084cd2b1866ac 100644 GIT binary patch delta 35 pcmZ4NHP4GXpO=@50SIO>-P_20hn>-G@(XrF=A6X5&4L_4vH-9S38(-7 delta 40 ucmbQ|wb+Y0pO=@50SM+uoZHBKhn>-R@(XrFj^dKU Date: Sun, 25 Oct 2026 10:55:55 -0400 Subject: [PATCH 09/12] =?UTF-8?q?feat(app):=20unify=20activity=20status=20?= =?UTF-8?q?colors=20=E2=80=94=20blue=20running,=20amber=20attention=20(por?= =?UTF-8?q?t=20of=20harmoniqs/opencode#379,=20#378)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ported from the fork branch amico/issue-378-activity-colors (df38ec003c) into the app-bundle overlay. The fork-side edits to run-view/device-view/ calibration-view are dropped: amicode removed those surfaces (#1818, pinned by research-surfaces-absence.test). status-popover-body keeps the engine toggle's engine-state dot and uses ActivityDot for real MCP servers. app/ui typecheck clean; app unit suite matches the lane baseline (24 pre-existing failures, zero new); activity + tab-status + widget-frame tests green. --- packages/app-bundle/manifest.json | 76 ++++++---- .../overlay/packages/app/CONTEXT.md | 28 ++++ .../app/src/components/connection-banner.tsx | 15 +- .../app/src/components/dialog-new-project.tsx | 2 +- .../app/src/components/server/server-row.tsx | 135 ++++++++++++++++++ .../session/context-warning-banner.tsx | 4 +- .../settings-v2/developer-tools.tsx | 22 +-- .../src/components/status-popover-body.tsx | 17 ++- .../status-popover-indicator.test.ts | 45 +++--- .../components/status-popover-indicator.ts | 33 +++-- .../app/src/components/status-popover.tsx | 53 +++++-- .../packages/app/src/design-polish.css | 91 ++++++++---- .../app/src/pages/home/legacy-home.tsx | 134 +++++++++++++++++ .../pages/layout/session-tab-status.test.ts | 33 ++++- .../src/pages/layout/session-tab-status.tsx | 48 +++++-- .../app/src/pages/layout/sidebar-items.tsx | 83 ++++++----- .../src/pages/layout/sidebar-workspace.tsx | 4 +- .../pages/session/v2/lineage-ledger-panel.css | 4 +- .../src/server/amicode/widget-frame-html.ts | 4 +- .../test/server/amicode-widget-frame.test.ts | 6 + .../session-review-file-preview-v2.tsx | 16 ++- .../packages/ui/src/amicode/activity-dot.tsx | 22 +++ .../packages/ui/src/amicode/activity.test.ts | 57 ++++++++ .../packages/ui/src/amicode/activity.ts | 76 ++++++++++ .../ui/src/amicode/amico-presence.css | 28 +--- .../ui/src/amicode/amico-presence.stories.tsx | 2 - .../packages/ui/src/amicode/amicode.css | 113 +++++++++------ .../ui/src/amicode/connection-icon.tsx | 28 ++-- .../ui/src/amicode/connections-tab.tsx | 22 ++- .../packages/ui/src/amicode/run-window.tsx | 25 ++-- .../packages/ui/src/amicode/vaults-tab.tsx | 17 ++- .../ui/src/components/amicode-activity.tsx | 12 ++ .../packages/ui/src/components/card.css | 115 +++++++++++++++ .../packages/ui/src/components/card.tsx | 126 ++++++++++++++++ 34 files changed, 1189 insertions(+), 307 deletions(-) create mode 100644 packages/app-bundle/overlay/packages/app/src/components/server/server-row.tsx create mode 100644 packages/app-bundle/overlay/packages/app/src/pages/home/legacy-home.tsx create mode 100644 packages/app-bundle/overlay/packages/ui/src/amicode/activity-dot.tsx create mode 100644 packages/app-bundle/overlay/packages/ui/src/amicode/activity.test.ts create mode 100644 packages/app-bundle/overlay/packages/ui/src/amicode/activity.ts create mode 100644 packages/app-bundle/overlay/packages/ui/src/components/amicode-activity.tsx create mode 100644 packages/app-bundle/overlay/packages/ui/src/components/card.css create mode 100644 packages/app-bundle/overlay/packages/ui/src/components/card.tsx diff --git a/packages/app-bundle/manifest.json b/packages/app-bundle/manifest.json index cfce89fa1..1edd99eae 100644 --- a/packages/app-bundle/manifest.json +++ b/packages/app-bundle/manifest.json @@ -8,7 +8,7 @@ "upstream_base_sha": "3104c1428ec91f809e5ab86631300de41eb6952e", "promoted_at": "2026-09-13T22:15:25.839Z", "files": { - "packages/app/CONTEXT.md": "69e73ecec62f730398e8f25071e736a58b2f7a1d9059b60788e9936051051f14", + "packages/app/CONTEXT.md": "ce2c2fa79b95f053055fa92889dfa4e5ad583416d5008dd7203fe160ab155ade", "packages/app/index.html": "fa38026be3b7e7ec38958a558d7b4d059140b2aff998c3ac862601543702584d", "packages/app/package.json": "3c9bf3d487052d4834b7fd3ce93fcf6b7fc358de92b8be4b0e84fc424a971f74", "packages/app/playwright.config.ts": "b7dae0b39248b4ddf7cba829af96ac7fe3cc919add9711ecb5292d9bd52e1805", @@ -25,7 +25,7 @@ "packages/app/public/oc-theme-preload.js": "27227e802b3494e7c545da903e679efdb30ccc754cd4eb5cdf08005a40d560b6", "packages/app/src/app-desktop-commands.test.ts": "1d2057f238dc14e4407ff5c4e53afe091784790bc0fd0dbfde0eacd91e2bd968", "packages/app/src/app.tsx": "6fe2c1136330b0b76beb10b15c0ca9df2c62877c98ba3eb4cb91844125916a82", - "packages/app/src/design-polish.css": "6dceafb232f7132be588debc33c913fa6773966d54d6bbf6a1496b7622adeacc", + "packages/app/src/design-polish.css": "eadd8df5263f6f9a85355e8752502004044ea2d95253ddbf09e5a0965451ef38", "packages/app/src/desktop-menu.ts": "350e0e9f2e4294bfe1b1f357b3cd2ea0908b14d20ca1e79fda2305db1958fb24", "packages/app/src/entry.tsx": "f35e1017f4c9d478d254b2a38043e5750064b6bef25169c3e07ae9f72ff1049c", "packages/app/src/index.css": "2c11df3dcebb381358e06626094b5dda3c35688ad7940d643b2cbb6cff1ea9e9", @@ -109,7 +109,7 @@ "packages/app/src/components/amicode-default-model.tsx": "16a2de1feb12e07416fe5fbb25879391f912ee0909b553c868c1b531f2ed1f2d", "packages/app/src/components/amicode-defaults-capsule.tsx": "94dc081bc47abb7e50566fcd8e9e53124f9d7632d61f93eff4f1627e0c2efb2d", "packages/app/src/components/amicode-file-ref-bridge.tsx": "11c17d7fdb29d16530e188b13ee8af728f37d9adfbb518084106ce50cc0c527a", - "packages/app/src/components/connection-banner.tsx": "c793966e1f9d6f307af697032b235c2ff65afaa04f6cc232e40ce45e5745ded4", + "packages/app/src/components/connection-banner.tsx": "82194291181165ed7b1b1b3a6801a1bdcd2c2b7cdd733c28486bd69f86477a24", "packages/app/src/components/dialog-command-palette-v2.css": "85984957adaf15efcc0828310f3a0b438cedaaba776a9fec2eddb3ad83487fa2", "packages/app/src/components/dialog-command-palette-v2.tsx": "e12da3e5e9e503089dd9139b20e04f6b566cf74fa445f23a2e193f5883f7c5f3", "packages/app/src/components/dialog-connect-provider-harmoniqs.test.ts": "7c16914afe3e77ad09691bbfa8223981eda0c2faf3616ed6db2e4e5fa9ce9fdd", @@ -117,7 +117,7 @@ "packages/app/src/components/dialog-connect-provider.tsx": "6eb784f06171e3c22d934fd170199b6b1020295a365b669aa8581c86b464b77d", "packages/app/src/components/dialog-edit-project-v2.tsx": "f34aaed28604facd9d8a46a4934e2b331421d5f60902a2b160ddcec11af03bd9", "packages/app/src/components/dialog-edit-project.tsx": "5371da3de3a643d4347629af55c0adc2476bffe7b2f061535a92fd67e756e562", - "packages/app/src/components/dialog-new-project.tsx": "102fc33e8eeaf3c76ddd1b953b7b97995d30c02a3579f8b7036e47448a1582a8", + "packages/app/src/components/dialog-new-project.tsx": "acc359b435e1bec55b9bb0c4926551b9aabf1ab3150c6b018ddd1d3ba6465f34", "packages/app/src/components/dialog-release-notes.tsx": "0ea5b049786787643a7f6b54d4ccf84c4f8ffa4e8272426c0d32f60d445b6890", "packages/app/src/components/dialog-select-directory-v2.css": "2e2f18abdca77f9fe85da157c40f288f3b1641ca92845208749e16455fd58637", "packages/app/src/components/dialog-select-file.tsx": "e6278ee35dd27d203e00ad73f70c709c44a6c06f81ffbd931edfaa0c986b9e03", @@ -151,12 +151,12 @@ "packages/app/src/components/settings-providers.tsx": "c86c9bf4a6f6e198dc7b0995a69d5b76e851144987bbe601802b1b0120ee4ac4", "packages/app/src/components/solver-switch-banner.tsx": "ec7edeec6dc05a4a06b39241b4f3a8e7534da97722dd8ec6f26ff782025802e9", "packages/app/src/components/split-frame.tsx": "44c53737f72027b461097c03df48a4f411d04a82d02b1fce4901813c73dfcc72", - "packages/app/src/components/status-popover-body.tsx": "c7f5cce9066a6388de8531156a300503ead6f596cad2ea4269d64c421a97d0c5", - "packages/app/src/components/status-popover-indicator.test.ts": "9afc7b49d9021a94d74f3f777178230ae13bd26e5749da0cee4d3378eacacacd", - "packages/app/src/components/status-popover-indicator.ts": "887b3e659ff49fd0d2a8715fad6562dc7e9402b80d06442058502306bdb108f5", + "packages/app/src/components/status-popover-body.tsx": "2b6aa39a4358bbce7b6836b6a719be8eb24db96cd603bc0b4fb9fb0365413fed", + "packages/app/src/components/status-popover-indicator.test.ts": "e3530caa3a46b822603a8d0a753550605951c2cc5e95c60c46636935f2398a7a", + "packages/app/src/components/status-popover-indicator.ts": "d705c258e3404567fa5ae6d8ce9c27010832f12fdd614cef21f0297154e7faae", "packages/app/src/components/status-popover-model.test.ts": "10634035bab0f31de24564e0d6c8413a0f7434fb2c08ac3ef722981a69dd08e7", "packages/app/src/components/status-popover-model.ts": "ec5a5a3caf9344fe617c7bc668af81d96ad4290378c34186cd4cd959821f1c85", - "packages/app/src/components/status-popover.tsx": "520c9e33b072cca1470508531eba194c690f0be86028f1b5abcbad0fe591bd5e", + "packages/app/src/components/status-popover.tsx": "f712c9b54b0ce63e2020a22bdb55bcc104e49ef41d4d5aea216b22900a3392ef", "packages/app/src/components/terminal.tsx": "2ce242034ae129c1f475c7a24fdec3facdd9bb1f49ba8c6b503643b07ae241e5", "packages/app/src/components/titlebar-channel.test.ts": "184238871c46f586dcc21081f710e37a77914c983fccf250665c7317212e41d8", "packages/app/src/components/titlebar-channel.ts": "385ace91d3c69c92f5b442bf15d2040e35df855f1d4ece47312c1ff8e1b01780", @@ -425,7 +425,7 @@ "packages/opencode/test/server/amicode-vault-browser.test.ts": "03347a6a9b6ba250b7a1150449cbe44b8817dbc0ebc433c89a703f4818492a5a", "packages/opencode/test/server/amicode-vaults.test.ts": "3f7c8ecfdc250ff360c0ed66d9cc6354d9069e66001281b3f6e30981b9f71c35", "packages/opencode/test/server/amicode-warrants.test.ts": "a7ad74da87f77f4488f334debf8c126671d563839891eb3a19b3d1a134dbc504", - "packages/opencode/test/server/amicode-widget-frame.test.ts": "5246d63240eae3390c8ba7f14950c41d546d6936cbe64df35da503febec72a1f", + "packages/opencode/test/server/amicode-widget-frame.test.ts": "a0374ab77ea5b39d8a7cbe9985537045506fd233299cfae081920714ca184e5f", "packages/opencode/test/server/amicode-widget-manifest.test.ts": "ac404d1737bdde8ea3464cf0b5fa12f0a6f9e630a0698ca5fdec38449cb66d0f", "packages/opencode/test/server/amicode-widgets.test.ts": "9e7ad16922e59583c337ff9383e57988b8abdfc0836ec3280655196cd192e8e4", "packages/opencode/test/server/auth-accept-set.test.ts": "8fe698812ed9a809d21ace1a9751bd3f1976ab1e58ad5bda65dbfff58df8e3bc", @@ -524,13 +524,16 @@ "packages/session-ui/src/context/data.tsx": "7b840b5bf6929445d21da2cbd8e8006316c6950a09d559ea77e8c397e728cd99", "packages/session-ui/src/pierre/worker.ts": "7f210f931777956567eaeea15f2f5478befc7874a64a4b685504c59c66503fee", "packages/session-ui/src/util/clipboard.ts": "c37ba53914714ed04135e5a254e8ea90186f097d7470fa0873a83de82f3fef2d", - "packages/ui/src/amicode/amico-presence.css": "b3f345b607e318dcfbefcdc677fe21cc7d387b9946b0f9d625b0a842fe7e31b9", - "packages/ui/src/amicode/amico-presence.stories.tsx": "6012506c946a18460f8a6105c5860a050e47e7708818b0408b516370f50b7094", + "packages/ui/src/amicode/activity-dot.tsx": "fc3c9e620cdd204e930f0d53a0a44456e0c886679174c91f0efbf9707daa7bc6", + "packages/ui/src/amicode/activity.test.ts": "1674ca5138780242767b0eb42fc444a9b8ce7868ac119117dd1487b7209180b2", + "packages/ui/src/amicode/activity.ts": "a2c9b4fe27175a400ea0bcfeb433dfcb572cd793766bf0fd232c741feef5cfbf", + "packages/ui/src/amicode/amico-presence.css": "1effe22b91a512c46ba0b4f4231da4b7665fc00c2f3bd8e1e9e76c6b7532e44c", + "packages/ui/src/amicode/amico-presence.stories.tsx": "e5d95248b32f2aa2a3879c3695721d6bdca77d90f3229ce83d42e030631763bb", "packages/ui/src/amicode/amico-presence.test.ts": "c7ea8c840004d06e231a7368b3c10e11b7dd7efcbcaca4867b0f30167bd2e2bb", "packages/ui/src/amicode/amico-presence.ts": "b4708117445d3fa5cf9be8c9b2f51bfb1f6b01a76e2fac114cde93107a075e26", "packages/ui/src/amicode/amico-wave.stories.tsx": "fed064c5202bd772308e2c6165c9d63182661fe6ce590c75cac1fd0e9deadf94", "packages/ui/src/amicode/amico-wave.tsx": "478cd8b19a87679016d9dbb0aa494ba7d1f1ef67b3da1d09fb35d28e33de9017", - "packages/ui/src/amicode/amicode.css": "a7b8e410dad9ee66190cc31d4a4782efa44e8f759d89d701c7525e67002b98c8", + "packages/ui/src/amicode/amicode.css": "5ac35d2f3dd9d64f904e5da89ee361bc8f81a319308bd2686fcc71f2d175a561", "packages/ui/src/amicode/approval-bridge.test.ts": "5b5e5a41c827b7a3cdaf455dbf488cfa7b8d339f46e64f5a35a0ba9226eb0ebf", "packages/ui/src/amicode/approval-bridge.ts": "ceaa6024916794ece1bc80a004b79c36406d2bc512f82e003c25d919c9050e48", "packages/ui/src/amicode/approval-card.tsx": "f55bd13bfded569020ad0ce0d385da3018aee8e833ec573cb45447114658a11b", @@ -547,10 +550,10 @@ "packages/ui/src/amicode/bridge.ts": "e7b93a15060a0cb1ab0be2b367f73e926d74b990a8af917a9966b451ee303b5b", "packages/ui/src/amicode/card.stories.tsx": "b1591abb4af426b0eb08588ea1585336a392d755b5d308cb31f17cb65acf5a3e", "packages/ui/src/amicode/card.tsx": "1f462017d9113082bb2e802e3d812b25cf5ee9b1bd848559e0533de4d382517f", - "packages/ui/src/amicode/connection-icon.tsx": "2b36c05209a3b5091c6407d59add58fb51ec34724ae6411af85c3dcc797a9987", + "packages/ui/src/amicode/connection-icon.tsx": "f0ffda2ebae57a2945a60cd278b5aa3986273b0cabe97ed46f417c037d167879", "packages/ui/src/amicode/connection-picker.tsx": "73293d1e159dab94a6d1600844fa1e47810ce0558bda6ba4e4209f7e368e3aff", "packages/ui/src/amicode/connections-paths.test.ts": "07c86e8e04700cdbd55bff8db342a8400d85114d76bc73fee8205e9162953f2a", - "packages/ui/src/amicode/connections-tab.tsx": "06489a47bbd6d482ba4d2dc2cf4e9e8e9c648be774f78cd76e2abc87451116e5", + "packages/ui/src/amicode/connections-tab.tsx": "da11957bee28ff988427f9001be81716254bea218057d6f1ff5e7a2fce374446", "packages/ui/src/amicode/connections.stories.tsx": "fda54a41c4b46f5cab5278278f92c01d1fea70610f7b8641fa5855be6bd9d486", "packages/ui/src/amicode/connections.test.ts": "a718b1b698178087fdac5294c1661ca2eb200491e2a076465a09ab52c70b8910", "packages/ui/src/amicode/connections.ts": "89b6c99e991e1aa594768a6555f7f555782dd615c21132e6a688155431cc0dfd", @@ -593,7 +596,7 @@ "packages/ui/src/amicode/run-plot.ts": "b4bc50f279cb864f7720e56ba69b993c7a4e599a71047162173b1b93a63598ac", "packages/ui/src/amicode/run-series.test.ts": "2e7aec7cdf7eb056efbe52d180ff316482e3010e2442611daaf4bda444c82705", "packages/ui/src/amicode/run-series.ts": "c94496068ae05f4600b9dd9fab4247b678dae7aaba31ef48e4389b0e7fefc52c", - "packages/ui/src/amicode/run-window.tsx": "e8343faa72bf6e7545f48e47d2576cd781f038b3ccb448e25130a52df17df5f0", + "packages/ui/src/amicode/run-window.tsx": "60c71ddaf9161ceaeeba10c1838003731a288dc6d85473d2f8d17e34c771c289", "packages/ui/src/amicode/shell-row.test.ts": "48ec6f0786edf81f163e350904bc2a93b1a55375b8115e688237ce8c2f1a7e64", "packages/ui/src/amicode/shell-row.ts": "b39dc05d2b77ba6d44456248782b31f6219c891d56700203186a65ccb48319ad", "packages/ui/src/amicode/solver-switch.test.ts": "c9cc10d657d7adf0c388e56cb6058affed5a255bdeb9b8f2fea5c02f4daff1fe", @@ -611,7 +614,7 @@ "packages/ui/src/amicode/thinking.ts": "ce2529285621620df168eaaa845cd2957b4fe5e61729fdfa736a8702ccf059b7", "packages/ui/src/amicode/ui-bridge.ts": "352f862a7a29245b635257485c377e6ac240e32a9def21173365177fb544a93f", "packages/ui/src/amicode/upload.ts": "9a3a59541ba056a5069f5145cc10bbcc8d44caeb7cfce7a59d97415de2e4a149", - "packages/ui/src/amicode/vaults-tab.tsx": "9e93b7424f0d885250cf999a115b1801d5973f3fdb9095a5c29d74e7093af7e6", + "packages/ui/src/amicode/vaults-tab.tsx": "28be62b5776501c6ef283f2905913ab033977c1661b479339877869dea8cad46", "packages/ui/src/amicode/vaults.test.ts": "24826e6b8b083b0a7478e469f3d4fd7319c973da281cb569e3d885d83d3d46d4", "packages/ui/src/amicode/vaults.ts": "6250b08a8d6493bd42c5407bfcb581a300757b273a587247e3666ddbe25c00af", "packages/ui/src/amicode/wave-geometry.test.ts": "3a89a89310b8c9ce15b402ced6dec83d691bc4ccf50448f2a89e4c0ff1c80cc7", @@ -630,6 +633,7 @@ "packages/ui/src/amicode/widget-tokens.test.ts": "426064638b322a421f794bedf5dbf41ad75fc30b3da6e2d02dcf6ed2f85d5f45", "packages/ui/src/amicode/widget-tokens.ts": "397edcfa18882b2b6b6abef0a8783d72323f793e974d0bd440ca240a10b2f737", "packages/ui/src/components/amico-spinner.tsx": "75114d38aac924a37fee563e35375ebfd5e96cf3771690d01ad484f91df1c563", + "packages/ui/src/components/amicode-activity.tsx": "2924852f0dc23fe3c9b0c5962fbbd6218532d7f5d8bfd4f4ef1e4aa31b6d07dc", "packages/ui/src/components/amicode-approval-bridge.tsx": "ec853afaf3f7bad097532e4887d9d125e88326945a47229fbb0831670c7b2fad", "packages/ui/src/components/amicode-ask-bridge.tsx": "2ddd8ad2663d965e6c42f11cde28bc516e9e881c60dcc7e85443f8bea6de92ef", "packages/ui/src/components/amicode-ask.tsx": "096496c187b7e4def0230c59df2e83757d1e3ebf567d946095c3e81f6ce341f0", @@ -659,6 +663,8 @@ "packages/ui/src/components/amicode-widget-grid.tsx": "df589de093bca703e9fdbbffc6a13020e96ee0ef43e545718f91277ec0b7cd24", "packages/ui/src/components/brain-engine.tsx": "955723ad42c6abfb785313f62277143142f1248987c45da30eadaf28d5137072", "packages/ui/src/components/brain-ref.tsx": "c97a0ac0dad470c9b9e194d6ba94d1f1cfbb774bdbea3f798bece4a2a9968a60", + "packages/ui/src/components/card.css": "0f87ac777181fb1eebbdc9d4f4c658aeff83eb0bd1171b9ef909c64504b11332", + "packages/ui/src/components/card.tsx": "f7cc63c3a3ba8a2f31e06584fedcf42b9f2bf69c04aa1c830ed2d6a7dc6ef837", "packages/ui/src/components/context-menu.css": "7c701902c852f205a178ded4fe8c6181ba43cb4d1c42c6ad9a39ae368738aa1a", "packages/ui/src/components/context-menu.stories.tsx": "0542961bd9db8e191e122b901519452217a410ea5339e8d3ee09a73258ba59a9", "packages/ui/src/components/context-tree-data.tsx": "ca261e7a5eeed09130af14bb755fc2258833d2ac9efd995418cb32927de05d86", @@ -728,7 +734,8 @@ "packages/app/src/components/prompt-input/slash-popover.tsx": "a1800173edebcd419802796470f7270acaf0ef689a72f1b81a1cedfd8968337d", "packages/app/src/components/prompt-input/submit-stream-gap.test.ts": "351f0f5a8cf7aff55c859cb82fbca388ef848831256b80f74ee52ab9cf5d07c6", "packages/app/src/components/prompt-input/submit.ts": "cdb01cf2f2485fbbdb7ddc77dcdea99197d8c03bd302b8fe032bee8c15f525c3", - "packages/app/src/components/session/context-warning-banner.tsx": "f5fab4d7a9536238b70d64488fc30d44f6a4948fdcffd910a9666f4b6c296019", + "packages/app/src/components/server/server-row.tsx": "caf3179cf0b34b00fcab6ed57a324ff1f90f571f604216c49164dc54bd9f12ab", + "packages/app/src/components/session/context-warning-banner.tsx": "c4ceb3f9589ebd21cc66e21cef209552fc5fcff6cd825da5069c2259c8d33e50", "packages/app/src/components/session/index.ts": "f0d58fa3a336639566ec51a10b692649707f91a7b780c0b5c4dcf18642d030e8", "packages/app/src/components/session/panel-menu.tsx": "42f323046b7375ae158023a51506038eb15f2e1545c407f45acd47446a8ef3e0", "packages/app/src/components/session/pdf-canvas-view.tsx": "591bac93a2e1e10a1b42404a29bcc5222ebbc5faf734c80556fbca35c90551dd", @@ -764,7 +771,7 @@ "packages/app/src/components/settings-v2/developer-tools-rebuild-flags.ts": "561a75fa456b832d4af6f782c7ecf5269ce768fc2adc59c15a79197a2dde84c0", "packages/app/src/components/settings-v2/developer-tools-request-state.test.ts": "29711b3bc17fddbb8d046e104bc78e775a9ec7c5aa0ee2e181fbcd8b2b5e2135", "packages/app/src/components/settings-v2/developer-tools-request-state.ts": "e9678ed32e7c2562b18143f39a51c0a94dbf0a0884d056653c6c77e4cd07a9bd", - "packages/app/src/components/settings-v2/developer-tools.tsx": "0e3a739a1ed3cb6311f93174080a3bce71219cee9bcaccd0719d3bf9e6932988", + "packages/app/src/components/settings-v2/developer-tools.tsx": "351b2696211fc67e31e1b7265c809de01c54312fb90c0eef50f95896a07684b2", "packages/app/src/components/settings-v2/dialog-settings-v2.tsx": "a1e1fb7eea7bbad7708b97a721b54a4897c8cb6f582b71d6bb7c8ca7f44930db", "packages/app/src/components/settings-v2/general.tsx": "2abf6c9c7b4b9bf020ed224dbc7fff4574db47711956fc08e1b8e84e781d395a", "packages/app/src/components/settings-v2/permission-scope-relocation.test.ts": "39539c4fc4b7433f08b913100f48a7fa75d6ab47615ba697aa9ae8eee09d316b", @@ -783,6 +790,7 @@ "packages/app/src/context/global-sync/types.ts": "da1408b5665534588a0f437972730b810b0d1271a98bb8d0c442f3d4625bbf7f", "packages/app/src/pages/home/home-projects-view.tsx": "074f1cdd7926db9574164824666cd63f1f5bd0f8e8dfb65453f6fe8f2dca0ec8", "packages/app/src/pages/home/home-sessions-view.tsx": "5b73b18e4349a628a15e09a3810ecf2319bde222e18fe8de5d08373ac91fb37c", + "packages/app/src/pages/home/legacy-home.tsx": "43c48bfadf63c56660c8ae4dce15ef2526e87aca36b375f863510dd76e64000e", "packages/app/src/pages/layout/helpers.test.ts": "3a77cfe018ce2867cccded2c726f556e53a9895a77baa944a4878714b9b2b59e", "packages/app/src/pages/layout/helpers.ts": "b9b9bd551833390d9dcb7e2bc56de77abdef770d7d786f732930ed2bb6b8f4d6", "packages/app/src/pages/layout/notification-toasts.test.ts": "6a95bd3dccb03be894b5d2adb4bc1f7531a9ec704e4d0eae1eba4ec89bbc9846", @@ -792,10 +800,10 @@ "packages/app/src/pages/layout/session-list-chip.tsx": "62373670362fd78e944753b2839820411b5c67dea369dec6a39c5d03df827b16", "packages/app/src/pages/layout/session-list-chip.wiring.test.tsx": "3f19399ea11aabbd6c11624ebc0597301e05cfc033da543c8231a21839880997", "packages/app/src/pages/layout/session-tab-avatar.tsx": "89e83e239d513f91b31f16473be531eaf82768cefe637ea5f10d752cea4ef887", - "packages/app/src/pages/layout/session-tab-status.test.ts": "d6e62365948450c0acda628bc5d03233e5e6ead69649c2d2fb7e20c906007e74", - "packages/app/src/pages/layout/session-tab-status.tsx": "0960d24e581e5cdd38d1bea5e745039542e5ab5268f5540cf8fa87d84717922b", - "packages/app/src/pages/layout/sidebar-items.tsx": "daf940fc66f34e5f83410a8580f7bb1790bedc599ab4d64fcda2301537b24865", - "packages/app/src/pages/layout/sidebar-workspace.tsx": "a0f8e3fbd16d09e783db2a7a09c686d3aa993436d882425d3436a59ec4adf94b", + "packages/app/src/pages/layout/session-tab-status.test.ts": "4f123d40f8cc817d82476daf0b79355783fc101ce8384efbed3896b9f8cbe3eb", + "packages/app/src/pages/layout/session-tab-status.tsx": "61bca75aee87050878a03a9455329478a380193afb52208d2f0000509a08b308", + "packages/app/src/pages/layout/sidebar-items.tsx": "a3e62d5b1fd9ee9f021d466c058c640cf43553a7ad9b69f810a4c12b6f9e46c4", + "packages/app/src/pages/layout/sidebar-workspace.tsx": "19a9daf1b8bf2a4d7b3ec1439a3cc4ce2472f71d93f8d0b0f9e27727397eca45", "packages/app/src/pages/layout/worktree-auto-rename.test.ts": "fb6fac363b8bee1225bbcff8098019c02f19b095b5cd96c29f7e673acb1e4e89", "packages/app/src/pages/layout/worktree-auto-rename.ts": "2ef3e506b96b972cb55799a89aa54ca6453fc31c80bc022aa16db6ecfe9cdced", "packages/app/src/pages/new-session/new-session-draft-controller.ts": "3607771a22b6855afd0b0d666de662c9070a2dcd56ae96ba51d89c9e11018e81", @@ -857,7 +865,7 @@ "packages/opencode/src/server/amicode/vault-browser.ts": "8a8252fb0b46583c75e9475d7c89e68c092f5255b21476f3c3d0b9d379ea2d48", "packages/opencode/src/server/amicode/vaults.ts": "e48c90c64de27abe24a510f188def6b56297d9bb1fa1ae08cbcc94888d42044a", "packages/opencode/src/server/amicode/warrants.ts": "13ef47a676245d577617f6e5af2c8a2beb3102346d2dbb43076159f3f43c01a7", - "packages/opencode/src/server/amicode/widget-frame-html.ts": "3df10eeeccbb81d2b163195ceaa1d5abc24e90506471ff6d6b2a5fc40e52db52", + "packages/opencode/src/server/amicode/widget-frame-html.ts": "c5395910b66c921a4b6f3f3fd214d19f98808dbe41754ecd541553494884acdf", "packages/opencode/src/server/amicode/widget-manifest.ts": "5753daf16ef4c1e74c97d4a515b30d123f7afbfee29bb07381c6f10ecc14cebf", "packages/opencode/src/server/amicode/widget-runtime.ts": "a5fe95eba46df87c4b4c54bc07bb91587b73e88eb90db889bc87358ece4b3f27", "packages/opencode/src/server/amicode/widgets.ts": "4315aa80eca9e0941d1198d253fd9ad37f57b7600ef3d7f116a6b8a9ee73095b", @@ -898,7 +906,7 @@ "packages/session-ui/src/v2/components/preview-nav-state.test.ts": "cec09a8b3c08ab16581d3d5b6ff704584b3c87d81c25e8af20f6f00272f3593f", "packages/session-ui/src/v2/components/preview-nav-state.ts": "ccaeb94d1ab39271f5dcd49c935c8fc7b54d4ffbd66e1352e4d797c9205553fc", "packages/session-ui/src/v2/components/preview-view-state.ts": "4ec961c7720417166dcccd45f0d4702afec664dfe7f4d0dd8668bb9b1d8af5e6", - "packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx": "fc06f46ed3c92c8066744da1f6ac97db7d8b7d2aeec5ef776d743dd93b317936", + "packages/session-ui/src/v2/components/session-review-file-preview-v2.tsx": "d4936b1512a334468c48d3c0f496e9b266927af20bdfbc84172c14139f78fc4f", "packages/session-ui/src/v2/components/session-review-v2.css": "bb306f226fda8f8ba6cf0ae706843ba9f2b48f3a3446433515e8a8dd1f9dc626", "packages/session-ui/src/v2/components/session-review-v2.tsx": "4151286efb6e0e148246ee3eebae24a2e5d4eac2603fd5ea8546d0839a694161", "packages/session-ui/src/v2/components/shiki-highlight-decorations.test.ts": "f50e8156b708f3e0ddbc509abf71751778a74923dac9d7e65385b178f6346718", @@ -969,7 +977,7 @@ "packages/app/src/pages/session/v2/accumulate-diffs.test.ts": "289ef80e26c9e85d3121d3705cefd467d147d4872410fce7309d9b7a382020fa", "packages/app/src/pages/session/v2/accumulate-diffs.ts": "a4d5ee94b31db8def04d4b1e5efc909a8c5e0e14368af0e37414e98d4d819936", "packages/app/src/pages/session/v2/adversarial-ui-e2e.test.ts": "0767c40885b0dec6a55ec25600061b76ef06aeddc9f10b20ad256de407acb9f0", - "packages/app/src/pages/session/v2/lineage-ledger-panel.css": "d928c053a4da2aded4df8bd0d32e8133e46c17a3592b0ccdb4811a68e4a523e3", + "packages/app/src/pages/session/v2/lineage-ledger-panel.css": "04ec96e285912075ca83c412f2d699500c9756b8414b9c87a028546bb662fc8f", "packages/app/src/pages/session/v2/lineage-ledger-panel.test.tsx": "f621617f3ee0bb27923a0337049f54cbfc37cb2a7895a98ae79521a0b633e955", "packages/app/src/pages/session/v2/lineage-ledger-panel.tsx": "f572b8aca1e413d9b4a2394e6ba0ea63b2eecbd7f21b79907cede82ff2ae4d1b", "packages/app/src/pages/session/v2/lineage-ledger-view.test.ts": "0b3ac457706875994ff5abf94704201a1f8b45c63d8241d5ecaa5b21ca9bcc50", @@ -1016,11 +1024,11 @@ "packages/ui/src/v2/components/text-shimmer-v2.tsx" ], "exceptions": [], - "extracted_at": "2026-10-25T13:30:07.394Z", + "extracted_at": "2026-10-25T14:54:33.760Z", "per_package": { "packages/app": { "A": 254, - "M": 162, + "M": 164, "D": 0 }, "packages/core": { @@ -1059,13 +1067,13 @@ "D": 0 }, "packages/ui": { - "A": 144, - "M": 64, + "A": 148, + "M": 66, "D": 0 } }, "counts": { - "overlay_total": 994, + "overlay_total": 1002, "deletions": 7, "server_coupled": 0 }, @@ -1586,6 +1594,9 @@ "packages/session-ui/src/context/data.tsx": "M", "packages/session-ui/src/pierre/worker.ts": "M", "packages/session-ui/src/util/clipboard.ts": "A", + "packages/ui/src/amicode/activity-dot.tsx": "A", + "packages/ui/src/amicode/activity.test.ts": "A", + "packages/ui/src/amicode/activity.ts": "A", "packages/ui/src/amicode/amico-presence.css": "A", "packages/ui/src/amicode/amico-presence.stories.tsx": "A", "packages/ui/src/amicode/amico-presence.test.ts": "A", @@ -1692,6 +1703,7 @@ "packages/ui/src/amicode/widget-tokens.test.ts": "A", "packages/ui/src/amicode/widget-tokens.ts": "A", "packages/ui/src/components/amico-spinner.tsx": "A", + "packages/ui/src/components/amicode-activity.tsx": "A", "packages/ui/src/components/amicode-approval-bridge.tsx": "A", "packages/ui/src/components/amicode-ask-bridge.tsx": "A", "packages/ui/src/components/amicode-ask.tsx": "A", @@ -1721,6 +1733,8 @@ "packages/ui/src/components/amicode-widget-grid.tsx": "A", "packages/ui/src/components/brain-engine.tsx": "A", "packages/ui/src/components/brain-ref.tsx": "A", + "packages/ui/src/components/card.css": "M", + "packages/ui/src/components/card.tsx": "M", "packages/ui/src/components/context-menu.css": "M", "packages/ui/src/components/context-menu.stories.tsx": "M", "packages/ui/src/components/context-tree-data.tsx": "A", @@ -1790,6 +1804,7 @@ "packages/app/src/components/prompt-input/slash-popover.tsx": "M", "packages/app/src/components/prompt-input/submit-stream-gap.test.ts": "A", "packages/app/src/components/prompt-input/submit.ts": "M", + "packages/app/src/components/server/server-row.tsx": "M", "packages/app/src/components/session/context-warning-banner.tsx": "A", "packages/app/src/components/session/index.ts": "M", "packages/app/src/components/session/panel-menu.tsx": "A", @@ -1845,6 +1860,7 @@ "packages/app/src/context/global-sync/types.ts": "M", "packages/app/src/pages/home/home-projects-view.tsx": "M", "packages/app/src/pages/home/home-sessions-view.tsx": "M", + "packages/app/src/pages/home/legacy-home.tsx": "M", "packages/app/src/pages/layout/helpers.test.ts": "M", "packages/app/src/pages/layout/helpers.ts": "M", "packages/app/src/pages/layout/notification-toasts.test.ts": "A", diff --git a/packages/app-bundle/overlay/packages/app/CONTEXT.md b/packages/app-bundle/overlay/packages/app/CONTEXT.md index bd0ba7993..0afaa05c9 100644 --- a/packages/app-bundle/overlay/packages/app/CONTEXT.md +++ b/packages/app-bundle/overlay/packages/app/CONTEXT.md @@ -106,6 +106,34 @@ It persists until onboarding is complete and then disappears; it never gates the _Avoid_: Onboarding modal, wizard (the wizard is the conversational interview; the nudge only surfaces its state), setup wall +### Activity vocabulary + +**Activity**: +The one semantic state set every status surface speaks — session dots (tab strip, +sessions dropdown, sidebar, home), run status (run window, run view), health dots +(connection, MCP, LSP, server, vault), build status, and status banners. Five roles, +each a `--activity-*` token in `src/design-polish.css` (solid dot colour plus a `-soft` +fill) aliasing the theme's v2 state tokens so light and dark move together: + +| Role | Colour | Means | +| --- | --- | --- | +| `running` | blue | in progress — working, solving, pending, validating | +| `attention` | amber | waiting on you — permission, question, needs auth, expired, drift, stalled | +| `success` | green | done-unread, healthy, finished | +| `error` | red | failed, unreachable | +| `idle` | grey | done-seen, aborted, stopped, disabled | + +A session's tab status ranks **error > attention > running > done-unread > idle**. Running +dots share one reduced-motion-aware pulse (`.amc-activity-pulse`). Colour is never the +only signal: every status dot carries an accessible label or sits beside text that +names the state. + +**Yellow never encodes a state.** Yellow is brand and selection only (the `--accent` +family). Amico's presence — the thought-rail harmonic dot, the working lane, the +thinking wave, the mark — is brand, not status, and keeps the accent. +_Avoid_: yellow "running", gold/amber warning edges from the theme as an attention +colour, the agent identity tint on a status spinner, raw hex status colours + ## Relationships - The **Landing** is a **Chat**; the window never opens on anything else. diff --git a/packages/app-bundle/overlay/packages/app/src/components/connection-banner.tsx b/packages/app-bundle/overlay/packages/app/src/components/connection-banner.tsx index be54a988d..8be6b59fe 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/connection-banner.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/connection-banner.tsx @@ -1,6 +1,7 @@ import { Show, createEffect, createSignal, onCleanup } from "solid-js" import { useLanguage } from "@/context/language" import { useServerSDK } from "@/context/server-sdk" +import { ActivityDot } from "@opencode-ai/ui/amicode-activity" // Amicode webview: a dead server (e.g. an opencode:build restart) used to read // as an endless "thinking" wave — the event loop retries silently every 250ms. @@ -64,12 +65,18 @@ export function ConnectionBanner() { "border-radius": "var(--radius-full)", "font-size": "12px", "font-weight": 600, - border: "1px solid var(--v2-border-border-base, #3c3c3c)", - background: "var(--v2-background-bg-layer-01, #1e1e1e)", - "box-shadow": "0 8px 24px rgba(0, 0, 0, 0.35)", - color: down() ? "var(--v2-state-fg-warning, #d29922)" : "var(--v2-state-fg-success, #3fb950)", + display: "flex", + "align-items": "center", + gap: "var(--space-2)", + border: "1px solid var(--v2-border-border-base)", + background: "var(--v2-background-bg-layer-01)", + "box-shadow": "var(--elev-float)", + color: "var(--v2-text-text-base)", }} > + {/* activity dot: attention while the link is down, success once back. + The words carry the state; the dot is the at-a-glance colour. */} + {down() ? language.t("app.server.connectionLost") : language.t("app.server.reconnected")} diff --git a/packages/app-bundle/overlay/packages/app/src/components/dialog-new-project.tsx b/packages/app-bundle/overlay/packages/app/src/components/dialog-new-project.tsx index e75364a8a..a7d761544 100644 --- a/packages/app-bundle/overlay/packages/app/src/components/dialog-new-project.tsx +++ b/packages/app-bundle/overlay/packages/app/src/components/dialog-new-project.tsx @@ -76,7 +76,7 @@ export function DialogNewProject(props: {