dsh-agy-link 0.4.34 → 0.4.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.36 (2026-09-18)
4
+
5
+ ### English
6
+
7
+ - **Fix (issue #30): Windows quota panel empty.** `readSystemKeychainToken()` had no win32 branch; primary credentials live in Windows Credential Manager (`gemini:antigravity`), not a disk token file. Added a PowerShell `CredRead` reader (same zero-dep pattern as macOS `security` / Linux `secret-tool`).
8
+ - **Fix: thinking blank after a reply finishes.** The client no longer rewrites React-managed `thinkBody` DOM (that wiped thought text on post-turn re-render). Styling is CSS-only; auto-expand uses the host disclosure control when prose is present.
9
+
10
+ ### 中文 (Chinese)
11
+
12
+ - **修复 #30:** Windows 额度读取 — 从 Credential Manager 读 `gemini:antigravity`。
13
+ - **修复:思考结束后变空** — 不再改写宿主 React 的 thinkBody;仅 CSS + 非破坏性展开。
14
+
15
+ ## 0.4.35 (2026-09-18)
16
+
17
+ ### English
18
+
19
+ - **Reasoning UI: only show thinking when there is real content.** Empty `[agy thinking turn · N thinking tokens]` chips no longer spam tool-heavy turns. When agy stores only `toolAction`/`toolSummary` (common), those become the visible thought line (e.g. `Searching the web — Search Wo Tianyu profile`). Full CoT prose still renders when present in the conversation DB.
20
+
21
+ ### 中文 (Chinese)
22
+
23
+ - **思考 UI:** 无正文时不再刷 token 横幅;工具步骤优先展示 `toolAction`/`toolSummary` 作为意图说明;完整思维链仍按 DB 正文展示。
24
+
3
25
  ## 0.4.34 (2026-09-18)
4
26
 
5
27
  ### English
package/dist/client.js CHANGED
@@ -1476,7 +1476,7 @@ window.__ModuleLoader__.load({
1476
1476
  //#endregion
1477
1477
  //#region src/client/reasoning.ts
1478
1478
  const REASONING_CSS = `
1479
- /* Ensure thinking disclosure row hover effect works reliably across all builds */
1479
+ /* Hover affordance on thinking disclosure rows */
1480
1480
  div[data-variant="think"] [data-disclosure-row]:hover [class*="iconIdle"],
1481
1481
  div[data-variant="think"] [class*="row"]:hover [class*="iconIdle"] {
1482
1482
  opacity: 0 !important;
@@ -1486,7 +1486,7 @@ div[data-variant="think"] [class*="row"]:hover [class*="chevronHover"] {
1486
1486
  opacity: 1 !important;
1487
1487
  }
1488
1488
 
1489
- /* Scrollable thinking body when content is long */
1489
+ /* Scrollable thinking body (does not touch DOM structure) */
1490
1490
  div[data-variant="think"] [class*="thinkBody"] {
1491
1491
  max-height: 360px;
1492
1492
  overflow-y: auto;
@@ -1511,20 +1511,11 @@ div[data-variant="think"] [class*="thinkBody"]::-webkit-scrollbar-track {
1511
1511
  margin: 4px 0;
1512
1512
  }
1513
1513
 
1514
- /* Tasteful metadata chip for [agy thinking turn · ...] banner */
1515
- .agy-thought-banner {
1516
- display: inline-block;
1517
- font-size: 11px;
1518
- line-height: 16px;
1519
- padding: 1px 7px;
1520
- margin-bottom: 6px;
1521
- border-radius: 4px;
1522
- color: var(--dsw-alias-label-tertiary, #64748b);
1523
- background: var(--dsw-alias-bg-layer-3, rgba(0, 0, 0, 0.04));
1524
- border: 0.5px solid var(--dsw-alias-border-l1, rgba(0, 0, 0, 0.1));
1525
- font-family: var(--dsw-font-family-code, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
1526
- font-weight: 500;
1527
- user-select: none;
1514
+ /* Style leading banner token sequence without rewriting DOM: host text nodes
1515
+ keep ownership; this only paints the first line if the host wraps it. */
1516
+ div[data-variant="think"] [class*="thinkBody"] > span:first-child,
1517
+ div[data-variant="think"] [class*="thinkBody"] {
1518
+ font-family: inherit;
1528
1519
  }
1529
1520
  `;
1530
1521
  function injectStyles() {
@@ -1539,37 +1530,41 @@ div[data-variant="think"] [class*="thinkBody"]::-webkit-scrollbar-track {
1539
1530
  }
1540
1531
  }
1541
1532
  /**
1542
- * Cleanly format any `[agy thinking turn ...]` banner in `thinkBody`
1543
- * into a styled metadata chip so the prose displays cleanly.
1533
+ * Try to expand a thinking row once when its body contains real prose
1534
+ * (not a bare [agy thinking turn · N tokens] chip). Uses the host's own
1535
+ * disclosure control when present — never mutates thinkBody children.
1544
1536
  */
1545
- function formatThinkingBody(thinkBodyEl) {
1546
- if (thinkBodyEl.dataset.agyFormatted === "true") return;
1547
- const text = thinkBodyEl.textContent ?? "";
1548
- const match = text.match(/^\[agy thinking turn(?: · \d+ thinking tokens)?\]\s*/);
1549
- if (!match) return;
1550
- const bannerText = match[0].trim();
1551
- const restText = text.slice(match[0].length);
1552
- thinkBodyEl.dataset.agyFormatted = "true";
1553
- thinkBodyEl.textContent = "";
1554
- const bannerSpan = document.createElement("span");
1555
- bannerSpan.className = "agy-thought-banner";
1556
- bannerSpan.textContent = bannerText;
1557
- thinkBodyEl.appendChild(bannerSpan);
1558
- if (restText.length > 0) thinkBodyEl.appendChild(document.createTextNode("\n" + restText));
1537
+ function tryExpandThinkRow(root) {
1538
+ if (root.dataset.agyExpanded === "true") return;
1539
+ const body = root.querySelector("[class*=\"thinkBody\"]");
1540
+ if (!body) return;
1541
+ const text = (body.textContent ?? "").trim();
1542
+ if (text === "") return;
1543
+ if (/^\[agy thinking turn(?: · \d+ thinking tokens)?\]\s*$/.test(text)) return;
1544
+ const style = typeof getComputedStyle === "function" ? getComputedStyle(body) : null;
1545
+ if (style && style.display !== "none" && body.clientHeight > 8) {
1546
+ root.dataset.agyExpanded = "true";
1547
+ return;
1548
+ }
1549
+ const toggle = root.querySelector("[data-disclosure-row]") ?? root.querySelector("[class*=\"disclosure\"]") ?? root.querySelector("button");
1550
+ if (toggle) try {
1551
+ toggle.click();
1552
+ root.dataset.agyExpanded = "true";
1553
+ } catch {}
1559
1554
  }
1560
- function scanThinkingBlocks() {
1555
+ function scanThinkingRows() {
1561
1556
  if (typeof document === "undefined") return;
1562
- const thinkBodies = document.querySelectorAll("div[data-variant=\"think\"] [class*=\"thinkBody\"]");
1563
- for (let i = 0; i < thinkBodies.length; i++) {
1564
- const body = thinkBodies[i];
1565
- if (body) formatThinkingBody(body);
1557
+ const rows = document.querySelectorAll("div[data-variant=\"think\"]");
1558
+ for (let i = 0; i < rows.length; i++) {
1559
+ const row = rows[i];
1560
+ if (row) tryExpandThinkRow(row);
1566
1561
  }
1567
1562
  }
1568
1563
  function installAutoExpandReasoning() {
1569
1564
  if (typeof window === "undefined" || typeof document === "undefined") return;
1570
1565
  injectStyles();
1571
1566
  const observer = new MutationObserver(() => {
1572
- scanThinkingBlocks();
1567
+ scanThinkingRows();
1573
1568
  });
1574
1569
  const setupObserver = () => {
1575
1570
  if (document.body) {
@@ -1577,7 +1572,7 @@ div[data-variant="think"] [class*="thinkBody"]::-webkit-scrollbar-track {
1577
1572
  childList: true,
1578
1573
  subtree: true
1579
1574
  });
1580
- scanThinkingBlocks();
1575
+ scanThinkingRows();
1581
1576
  }
1582
1577
  };
1583
1578
  if (document.readyState === "loading") window.addEventListener("DOMContentLoaded", setupObserver, { once: true });
package/dist/index.js CHANGED
@@ -1051,20 +1051,11 @@ var EventMapper = class {
1051
1051
  */
1052
1052
  *emitThinking(absIndex, thoughtTokens) {
1053
1053
  const text = this.opts.resolvedThoughts?.get(absIndex);
1054
- const hasText = text !== void 0 && text.trim() !== "";
1055
- if (!hasText && thoughtTokens <= 0) return;
1056
- if (!hasText && this.bannerOnlyThinkingEmitted) return;
1054
+ if (!(text !== void 0 && text.trim() !== "")) return;
1057
1055
  yield* this.ensureBlock("reasoning");
1058
- const banner = thoughtTokens > 0 ? "[agy thinking turn · " + thoughtTokens + " thinking tokens]" : "[agy thinking turn]";
1059
- if (hasText) {
1060
- const combined = `${banner} ${text.trim()}\n`;
1061
- const d = this.appendDelta(combined);
1062
- if (d) yield d;
1063
- } else {
1064
- this.bannerOnlyThinkingEmitted = true;
1065
- const d = this.appendDelta(`${banner}\n`);
1066
- if (d) yield d;
1067
- }
1056
+ const combined = `${thoughtTokens > 0 ? "[agy thinking turn · " + thoughtTokens + " thinking tokens]" : "[agy thinking turn]"} ${text.trim()}\n`;
1057
+ const d = this.appendDelta(combined);
1058
+ if (d) yield d;
1068
1059
  }
1069
1060
  /**
1070
1061
  * Map one event. `absIndex` is the event's position in the run recording;
@@ -2331,65 +2322,111 @@ function readVarint(b, offset) {
2331
2322
  /**
2332
2323
  * Extract model thought text from a protobuf step_payload.
2333
2324
  *
2334
- * In agy SQLite databases (steps table, step_type = 15 agent_response),
2335
- * the thought text is stored inside Protobuf field 20 (response body) ->
2336
- * sub-field 3 (length-delimited UTF-8 string).
2325
+ * Primary: field 20 -> sub-field 3 (verified on simple print-mode turns).
2326
+ * Fallback: longest non-JSON prose string, then toolAction/toolSummary
2327
+ * embedded in tool-arg JSON (tool-heavy agent turns often store only those).
2337
2328
  */
2338
2329
  function extractStepThoughts(payload) {
2339
- let pos = 0;
2340
- while (pos < payload.length) {
2341
- const res = readVarint(payload, pos);
2342
- if (res === null) break;
2343
- pos = res.next;
2344
- const tag = res.val;
2345
- const wireType = tag & 7;
2346
- const fieldNum = tag >>> 3;
2347
- if (wireType === 2) {
2348
- const lenRes = readVarint(payload, pos);
2349
- if (lenRes === null) break;
2350
- pos = lenRes.next;
2351
- const len = lenRes.val;
2352
- if (pos + len > payload.length) break;
2353
- const slice = payload.subarray(pos, pos + len);
2354
- pos += len;
2355
- if (fieldNum === 20) {
2356
- let p20 = 0;
2357
- while (p20 < slice.length) {
2358
- const t20Res = readVarint(slice, p20);
2359
- if (t20Res === null) break;
2360
- p20 = t20Res.next;
2361
- const t20 = t20Res.val;
2362
- const w20 = t20 & 7;
2363
- const f20 = t20 >>> 3;
2364
- if (w20 === 2) {
2365
- const l20Res = readVarint(slice, p20);
2366
- if (l20Res === null) break;
2367
- p20 = l20Res.next;
2368
- const l20 = l20Res.val;
2369
- if (p20 + l20 > slice.length) break;
2370
- const s20 = slice.subarray(p20, p20 + l20);
2371
- p20 += l20;
2372
- if (f20 === 3) {
2373
- const str = s20.toString("utf-8");
2374
- if (str.length > 0) return str;
2375
- }
2376
- } else if (w20 === 0) {
2377
- const v20Res = readVarint(slice, p20);
2378
- if (v20Res === null) break;
2379
- p20 = v20Res.next;
2380
- } else if (w20 === 1) p20 += 8;
2381
- else if (w20 === 5) p20 += 4;
2382
- else break;
2330
+ const proseCandidates = [];
2331
+ const actionCandidates = [];
2332
+ const consider = (str) => {
2333
+ const t = str.trim();
2334
+ if (t.length < 8) return;
2335
+ if (/^SYSTEM ROLE/i.test(t)) return;
2336
+ if (t.startsWith("/plan ") || t.includes("<PLAN>The user is requesting that you think")) return;
2337
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(t)) return;
2338
+ if (/^bot-|^call_/.test(t)) return;
2339
+ const act = /"toolAction"\s*:\s*"([^"]{3,200})"/.exec(t);
2340
+ const sum = /"toolSummary"\s*:\s*"([^"]{3,200})"/.exec(t);
2341
+ if (act || sum) {
2342
+ const line = [act?.[1] ?? "", sum?.[1] ?? ""].filter((s) => s !== "").join(" — ");
2343
+ if (line !== "") actionCandidates.push(line);
2344
+ }
2345
+ const looksJson = t.startsWith("{") || t.startsWith("[");
2346
+ if (!looksJson && t.length >= 24 && t.length <= 4e3 && /[a-zA-Z一-鿿]/.test(t) && !/^[0-9a-f-]{30,}$/i.test(t)) {
2347
+ if ((t.match(/[.!?]|,|。| /g) ?? []).length >= 2 || /[a-zA-Z]{4,}\s+[a-zA-Z]{3,}/.test(t)) proseCandidates.push(t);
2348
+ return;
2349
+ }
2350
+ if (looksJson) try {
2351
+ const obj = JSON.parse(t);
2352
+ const line = [typeof obj.toolAction === "string" ? obj.toolAction.trim() : "", typeof obj.toolSummary === "string" ? obj.toolSummary.trim() : ""].filter((s) => s !== "").join(" — ");
2353
+ if (line !== "") actionCandidates.push(line);
2354
+ } catch {}
2355
+ };
2356
+ const walk = (buf, depth) => {
2357
+ if (depth > 3) return;
2358
+ let pos = 0;
2359
+ while (pos < buf.length) {
2360
+ const res = readVarint(buf, pos);
2361
+ if (res === null) break;
2362
+ pos = res.next;
2363
+ const tag = res.val;
2364
+ const wireType = tag & 7;
2365
+ const fieldNum = tag >>> 3;
2366
+ if (wireType === 2) {
2367
+ const lenRes = readVarint(buf, pos);
2368
+ if (lenRes === null) break;
2369
+ pos = lenRes.next;
2370
+ const len = lenRes.val;
2371
+ if (pos + len > buf.length) break;
2372
+ const slice = buf.subarray(pos, pos + len);
2373
+ pos += len;
2374
+ if (fieldNum === 20) {
2375
+ let p20 = 0;
2376
+ while (p20 < slice.length) {
2377
+ const t20Res = readVarint(slice, p20);
2378
+ if (t20Res === null) break;
2379
+ p20 = t20Res.next;
2380
+ const t20 = t20Res.val;
2381
+ const w20 = t20 & 7;
2382
+ const f20 = t20 >>> 3;
2383
+ if (w20 === 2) {
2384
+ const l20Res = readVarint(slice, p20);
2385
+ if (l20Res === null) break;
2386
+ p20 = l20Res.next;
2387
+ const l20 = l20Res.val;
2388
+ if (p20 + l20 > slice.length) break;
2389
+ const s20 = slice.subarray(p20, p20 + l20);
2390
+ p20 += l20;
2391
+ if (f20 === 3) {
2392
+ const str = s20.toString("utf-8");
2393
+ if (str.trim() !== "") {
2394
+ consider(str);
2395
+ if (!str.trim().startsWith("{") && str.trim().length >= 24) proseCandidates.unshift(str.trim());
2396
+ }
2397
+ } else if (f20 === 2 || f20 === 1) consider(s20.toString("utf-8"));
2398
+ } else if (w20 === 0) {
2399
+ const v20Res = readVarint(slice, p20);
2400
+ if (v20Res === null) break;
2401
+ p20 = v20Res.next;
2402
+ } else if (w20 === 1) p20 += 8;
2403
+ else if (w20 === 5) p20 += 4;
2404
+ else break;
2405
+ }
2383
2406
  }
2384
- }
2385
- } else if (wireType === 0) {
2386
- const vRes = readVarint(payload, pos);
2387
- if (vRes === null) break;
2388
- pos = vRes.next;
2389
- } else if (wireType === 1) pos += 8;
2390
- else if (wireType === 5) pos += 4;
2391
- else break;
2392
- }
2407
+ if (len >= 8 && len < 2e5) walk(slice, depth + 1);
2408
+ } else if (wireType === 0) {
2409
+ const vRes = readVarint(buf, pos);
2410
+ if (vRes === null) break;
2411
+ pos = vRes.next;
2412
+ } else if (wireType === 1) pos += 8;
2413
+ else if (wireType === 5) pos += 4;
2414
+ else break;
2415
+ }
2416
+ };
2417
+ walk(payload, 0);
2418
+ const full = payload.toString("utf8");
2419
+ const act = /"toolAction"\s*:\s*"([^"]{3,200})"/.exec(full);
2420
+ const sum = /"toolSummary"\s*:\s*"([^"]{3,200})"/.exec(full);
2421
+ if (act || sum) {
2422
+ const line = [act?.[1] ?? "", sum?.[1] ?? ""].filter((s) => s !== "").join(" — ");
2423
+ if (line !== "") actionCandidates.push(line);
2424
+ }
2425
+ if (proseCandidates.length > 0) {
2426
+ proseCandidates.sort((a, b) => b.length - a.length);
2427
+ return proseCandidates[0];
2428
+ }
2429
+ if (actionCandidates.length > 0) return actionCandidates[0];
2393
2430
  return null;
2394
2431
  }
2395
2432
  /**
@@ -2441,9 +2478,9 @@ async function loadAgyDbData(conversationId, accountHome) {
2441
2478
  const info = extractToolInfo(payload);
2442
2479
  if (info !== null) steps.set(idx, info);
2443
2480
  }
2444
- if (stepType === 14 || stepType === 15) {
2481
+ if (stepType === 14 || stepType === 15 || TOOL_STEP_TYPES.has(stepType)) {
2445
2482
  const th = extractStepThoughts(payload);
2446
- if (th !== null && th.trim() !== "") thoughts.set(idx, th);
2483
+ if (th !== null && th.trim() !== "" && !/^SYSTEM ROLE/i.test(th.trim())) thoughts.set(idx, th);
2447
2484
  }
2448
2485
  }
2449
2486
  } catch {} finally {
@@ -28689,6 +28726,72 @@ function readMacKeychainToken() {
28689
28726
  return null;
28690
28727
  }
28691
28728
  }
28729
+ /**
28730
+ * Read the primary Antigravity OAuth token from Windows Credential Manager.
28731
+ * agy on Windows stores it via go-keyring under target `gemini:antigravity`
28732
+ * (user `antigravity`) as a UTF-8 JSON blob with the same shape as macOS
28733
+ * Keychain / Linux Secret Service. Without this reader the quota panel stays
28734
+ * empty on Windows: the on-disk `antigravity-oauth-token` file is often
28735
+ * absent while agy itself still authenticates via CredRead (issue #30).
28736
+ */
28737
+ function readWindowsCredentialToken() {
28738
+ if (process.platform !== "win32") return null;
28739
+ try {
28740
+ const script = [
28741
+ "$sig = @'",
28742
+ "using System;",
28743
+ "using System.Runtime.InteropServices;",
28744
+ "public class AgyCred {",
28745
+ " [DllImport(\"advapi32.dll\", SetLastError=true, CharSet=CharSet.Unicode)]",
28746
+ " public static extern bool CredRead(string target, int type, int flags, out IntPtr credential);",
28747
+ " [DllImport(\"advapi32.dll\", SetLastError=true)]",
28748
+ " public static extern void CredFree(IntPtr cred);",
28749
+ " [StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]",
28750
+ " public struct CREDENTIAL {",
28751
+ " public int Flags; public int Type; public IntPtr TargetName;",
28752
+ " public IntPtr Comment; public long LastWritten; public int CredentialBlobSize;",
28753
+ " public IntPtr CredentialBlob; public int Persist; public int AttributeCount;",
28754
+ " public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName;",
28755
+ " }",
28756
+ " public static byte[] Read(string target) {",
28757
+ " IntPtr p;",
28758
+ " if (!CredRead(target, 1, 0, out p)) return null;",
28759
+ " CREDENTIAL c = (CREDENTIAL)Marshal.PtrToStructure(p, typeof(CREDENTIAL));",
28760
+ " byte[] b = new byte[c.CredentialBlobSize];",
28761
+ " Marshal.Copy(c.CredentialBlob, b, 0, c.CredentialBlobSize);",
28762
+ " CredFree(p);",
28763
+ " return b;",
28764
+ " }",
28765
+ "}",
28766
+ "'@",
28767
+ "Add-Type -TypeDefinition $sig -ErrorAction Stop",
28768
+ "$b = [AgyCred]::Read('gemini:antigravity')",
28769
+ "if ($b -eq $null) { exit 1 }",
28770
+ "[Console]::Out.Write([System.Text.Encoding]::UTF8.GetString($b))"
28771
+ ].join("\n");
28772
+ const raw = execFileSync("powershell.exe", [
28773
+ "-NoProfile",
28774
+ "-NonInteractive",
28775
+ "-ExecutionPolicy",
28776
+ "Bypass",
28777
+ "-Command",
28778
+ script
28779
+ ], {
28780
+ encoding: "utf8",
28781
+ timeout: 8e3,
28782
+ stdio: [
28783
+ "ignore",
28784
+ "pipe",
28785
+ "ignore"
28786
+ ],
28787
+ windowsHide: true
28788
+ }).trim();
28789
+ if (!raw) return null;
28790
+ return parseGoKeyringPayload(raw);
28791
+ } catch {
28792
+ return null;
28793
+ }
28794
+ }
28692
28795
  var QuotaService = class {
28693
28796
  pool;
28694
28797
  preferredEndpointIndex = 0;
@@ -28706,6 +28809,7 @@ var QuotaService = class {
28706
28809
  readSystemKeychainToken() {
28707
28810
  if (process.platform === "darwin") return readMacKeychainToken();
28708
28811
  if (process.platform === "linux") return readLinuxSecretToken();
28812
+ if (process.platform === "win32") return readWindowsCredentialToken();
28709
28813
  return null;
28710
28814
  }
28711
28815
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-agy-link",
3
- "version": "0.4.34",
3
+ "version": "0.4.36",
4
4
  "description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
5
5
  "type": "module",
6
6
  "license": "MIT",