@linxin666/dsh-remote-web-ui 0.1.17 → 0.1.18

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.
Files changed (36) hide show
  1. package/README.i18n.yaml +2 -2
  2. package/README.md +5 -2
  3. package/README.zh.md +2 -2
  4. package/lib/client.js +48 -58
  5. package/lib/client.js.map +1 -1
  6. package/lib/mobile.js +649 -45
  7. package/lib/mobile.js.map +1 -1
  8. package/lib/types/client/PluginSettingsCard.d.ts +11 -0
  9. package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
  10. package/lib/types/client/PluginSettingsCard.js +11 -5
  11. package/lib/types/mobile/markdown.d.ts +26 -0
  12. package/lib/types/mobile/markdown.d.ts.map +1 -0
  13. package/lib/types/mobile/markdown.js +272 -0
  14. package/lib/types/mobile/markdown.test.d.ts +2 -0
  15. package/lib/types/mobile/markdown.test.d.ts.map +1 -0
  16. package/lib/types/mobile/markdown.test.js +67 -0
  17. package/lib/types/mobile/messages.d.ts +14 -0
  18. package/lib/types/mobile/messages.d.ts.map +1 -1
  19. package/lib/types/mobile/messages.js +54 -2
  20. package/lib/types/mobile/messages.test.js +99 -0
  21. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  22. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  23. package/lib/types/mobile/mobile-styles.js +167 -0
  24. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  25. package/lib/types/mobile/views/ChatView.js +81 -5
  26. package/lib/types/mobile/views/ChatView.test.js +105 -0
  27. package/package.json +1 -1
  28. package/src/client/PluginSettingsCard.tsx +50 -47
  29. package/src/client/settings-card.module.css +9 -0
  30. package/src/mobile/markdown.test.ts +77 -0
  31. package/src/mobile/markdown.ts +278 -0
  32. package/src/mobile/messages.test.ts +106 -0
  33. package/src/mobile/messages.ts +66 -2
  34. package/src/mobile/mobile-styles.ts +167 -0
  35. package/src/mobile/views/ChatView.test.tsx +121 -0
  36. package/src/mobile/views/ChatView.tsx +164 -6
package/lib/mobile.js CHANGED
@@ -13151,6 +13151,11 @@ function applyEvent(state, event) {
13151
13151
  case "tool/call":
13152
13152
  applyToolCall(state, event);
13153
13153
  break;
13154
+ case "request/context": {
13155
+ const window = pickNumber((isRecord$1(event.data) ? event.data : {})["contextWindow"]);
13156
+ if (window !== void 0) state.contextWindow = window;
13157
+ break;
13158
+ }
13154
13159
  default: break;
13155
13160
  }
13156
13161
  }
@@ -13158,10 +13163,12 @@ function applyUserMessage(state, event) {
13158
13163
  const data = isRecord$1(event.data) ? event.data : {};
13159
13164
  const id = pickString(data["id"]) ?? syntheticId("user", event.seq);
13160
13165
  const text = textFromContent(data["content"]);
13166
+ const sourceKind = pickString((isRecord$1(data["source"]) ? data["source"] : {})["kind"]);
13161
13167
  const existing = state.byId.get(id);
13162
13168
  if (existing !== void 0) {
13163
13169
  replaceMessage(state, existing, {
13164
13170
  ...existing,
13171
+ ...sourceKind !== void 0 ? { sourceKind } : {},
13165
13172
  text,
13166
13173
  seq: event.seq,
13167
13174
  time: event.time
@@ -13172,6 +13179,7 @@ function applyUserMessage(state, event) {
13172
13179
  id,
13173
13180
  kind: "user",
13174
13181
  text,
13182
+ ...sourceKind !== void 0 ? { sourceKind } : {},
13175
13183
  seq: event.seq,
13176
13184
  time: event.time
13177
13185
  };
@@ -13187,6 +13195,8 @@ function applyAssistantMessage(state, event) {
13187
13195
  const finalText = textFromContent(messageData["content"]);
13188
13196
  const finalReasoning = reasoningFromContent(messageData["content"]);
13189
13197
  const key = tsKey(turn, step);
13198
+ const usage = usageFromData(data);
13199
+ const contextWindow = state.contextWindow;
13190
13200
  let target = state.byId.get(id);
13191
13201
  if (target === void 0 && key !== void 0) target = state.pendingByTurnStep.get(key);
13192
13202
  if (target !== void 0) {
@@ -13195,6 +13205,8 @@ function applyAssistantMessage(state, event) {
13195
13205
  id,
13196
13206
  text: finalText,
13197
13207
  ...finalReasoning !== "" ? { reasoning: finalReasoning } : {},
13208
+ ...usage !== void 0 ? { usage } : {},
13209
+ ...usage !== void 0 && contextWindow !== void 0 ? { contextWindow } : {},
13198
13210
  seq: event.seq,
13199
13211
  time: event.time,
13200
13212
  pending: false
@@ -13209,6 +13221,8 @@ function applyAssistantMessage(state, event) {
13209
13221
  kind: "assistant",
13210
13222
  text: finalText,
13211
13223
  ...finalReasoning !== "" ? { reasoning: finalReasoning } : {},
13224
+ ...usage !== void 0 ? { usage } : {},
13225
+ ...usage !== void 0 && contextWindow !== void 0 ? { contextWindow } : {},
13212
13226
  seq: event.seq,
13213
13227
  time: event.time
13214
13228
  };
@@ -13220,6 +13234,27 @@ function applyAssistantMessage(state, event) {
13220
13234
  }
13221
13235
  if (turn !== void 0) state.messageTurn.set(id, turn);
13222
13236
  }
13237
+ /**
13238
+ * Extract token usage from an assistant event payload. Only attaches when the
13239
+ * wire carries finite `inputTokens` AND `outputTokens`; the cache fields are
13240
+ * included only for finite numbers.
13241
+ */
13242
+ function usageFromData(data) {
13243
+ const usageData = data["usage"];
13244
+ if (!isRecord$1(usageData)) return void 0;
13245
+ const inputTokens = pickNumber(usageData["inputTokens"]);
13246
+ const outputTokens = pickNumber(usageData["outputTokens"]);
13247
+ if (inputTokens === void 0 || outputTokens === void 0) return void 0;
13248
+ const usage = {
13249
+ inputTokens,
13250
+ outputTokens
13251
+ };
13252
+ const cacheReadTokens = pickNumber(usageData["cacheReadTokens"]);
13253
+ const cacheWriteTokens = pickNumber(usageData["cacheWriteTokens"]);
13254
+ if (cacheReadTokens !== void 0) usage.cacheReadTokens = cacheReadTokens;
13255
+ if (cacheWriteTokens !== void 0) usage.cacheWriteTokens = cacheWriteTokens;
13256
+ return usage;
13257
+ }
13223
13258
  function applyChunk(state, event) {
13224
13259
  const target = chunkTarget(event.data);
13225
13260
  if (target === null) return;
@@ -13397,6 +13432,238 @@ function foldEvents(events, existing) {
13397
13432
  return [...state.messages].sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
13398
13433
  }
13399
13434
  //#endregion
13435
+ //#region src/mobile/markdown.ts
13436
+ /**
13437
+ * A compact GFM-subset markdown renderer for the mobile chat: headings,
13438
+ * paragraphs, fenced + inline code, bold/italic/strikethrough, links,
13439
+ * images, lists, blockquotes, hr, and tables. All HTML is escaped before
13440
+ * transformation — the output only ever contains the renderer's own tags.
13441
+ * Dependency-free on purpose (the mobile bundle stays at ~456 KB); the
13442
+ * escape-first + protocol allow-list design mirrors the desktop panel's
13443
+ * preview renderer (dsh-aionui-panel/src/client/preview/markdown.ts).
13444
+ * Pure and exported for tests.
13445
+ * @module dsh-remote-web-ui/mobile/markdown
13446
+ */
13447
+ /** Escape HTML special characters. */
13448
+ function escapeHtml(text) {
13449
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
13450
+ }
13451
+ /**
13452
+ * Guard a raw link/image target against dangerous protocols. Returns the
13453
+ * (trimmed) raw string when safe, else null. Only http:, https:, mailto:
13454
+ * and fragment anchors (#...) are allowed; anything with another scheme
13455
+ * — javascript:, data:, vbscript:, etc. — is rejected so the value never
13456
+ * reaches dangerouslySetInnerHTML.
13457
+ */
13458
+ function safeUrl(raw) {
13459
+ const trimmed = raw.trim();
13460
+ if (trimmed === "") return null;
13461
+ if (trimmed.startsWith("#")) return trimmed;
13462
+ const scheme = /^([a-zA-Z][a-zA-Z0-9+.-]*):/.exec(trimmed);
13463
+ if (scheme === null) return trimmed;
13464
+ const name = scheme[1].toLowerCase();
13465
+ return name === "http" || name === "https" || name === "mailto" ? trimmed : null;
13466
+ }
13467
+ /** Find the ')' closing a link/image target, skipping nested parens. */
13468
+ function findCloseParen(text, from) {
13469
+ let depth = 0;
13470
+ for (let i = from; i < text.length; i += 1) {
13471
+ const char = text[i];
13472
+ if (char === "(") depth += 1;
13473
+ else if (char === ")") {
13474
+ if (depth === 0) return i;
13475
+ depth -= 1;
13476
+ }
13477
+ }
13478
+ return -1;
13479
+ }
13480
+ /** Inline pass: code spans, bold, italic, strikethrough, images, links. */
13481
+ function renderInline(text) {
13482
+ let out = "";
13483
+ let i = 0;
13484
+ const n = text.length;
13485
+ while (i < n) {
13486
+ const char = text[i];
13487
+ if (char === "`") {
13488
+ const end = text.indexOf("`", i + 1);
13489
+ if (end !== -1) {
13490
+ out += "<code>" + escapeHtml(text.slice(i + 1, end)) + "</code>";
13491
+ i = end + 1;
13492
+ continue;
13493
+ }
13494
+ }
13495
+ if (char === "!" && text[i + 1] === "[") {
13496
+ const close = text.indexOf("](", i + 2);
13497
+ if (close !== -1) {
13498
+ const parenEnd = findCloseParen(text, close + 2);
13499
+ if (parenEnd !== -1) {
13500
+ const alt = text.slice(i + 2, close);
13501
+ const safe = safeUrl(text.slice(close + 2, parenEnd));
13502
+ if (safe === null) out += escapeHtml(alt);
13503
+ else {
13504
+ const srcEsc = escapeHtml(safe).replace(/\s+/g, "%20");
13505
+ out += "<img alt=\"" + escapeHtml(alt) + "\" src=\"" + srcEsc + "\" />";
13506
+ }
13507
+ i = parenEnd + 1;
13508
+ continue;
13509
+ }
13510
+ }
13511
+ }
13512
+ if (char === "[") {
13513
+ const close = text.indexOf("](", i + 1);
13514
+ if (close !== -1) {
13515
+ const parenEnd = findCloseParen(text, close + 2);
13516
+ if (parenEnd !== -1) {
13517
+ const label = text.slice(i + 1, close);
13518
+ const safe = safeUrl(text.slice(close + 2, parenEnd));
13519
+ if (safe === null) out += renderInline(label);
13520
+ else out += "<a href=\"" + escapeHtml(safe) + "\" target=\"_blank\" rel=\"noopener noreferrer\">" + renderInline(label) + "</a>";
13521
+ i = parenEnd + 1;
13522
+ continue;
13523
+ }
13524
+ }
13525
+ }
13526
+ if (char === "*" && text[i + 1] === "*") {
13527
+ const end = text.indexOf("**", i + 2);
13528
+ if (end !== -1) {
13529
+ out += "<strong>" + renderInline(text.slice(i + 2, end)) + "</strong>";
13530
+ i = end + 2;
13531
+ continue;
13532
+ }
13533
+ }
13534
+ if (char === "*" && text[i - 1] !== "*" && text[i + 1] !== "*") {
13535
+ const end = text.indexOf("*", i + 1);
13536
+ if (end !== -1 && text[end + 1] !== "*") {
13537
+ out += "<em>" + renderInline(text.slice(i + 1, end)) + "</em>";
13538
+ i = end + 1;
13539
+ continue;
13540
+ }
13541
+ }
13542
+ if (char === "~" && text[i + 1] === "~") {
13543
+ const end = text.indexOf("~~", i + 2);
13544
+ if (end !== -1) {
13545
+ out += "<del>" + renderInline(text.slice(i + 2, end)) + "</del>";
13546
+ i = end + 2;
13547
+ continue;
13548
+ }
13549
+ }
13550
+ out += escapeHtml(char);
13551
+ i += 1;
13552
+ }
13553
+ return out;
13554
+ }
13555
+ /** Render a markdown document to HTML (block pass). */
13556
+ function renderMarkdown(source) {
13557
+ const lines = source.replace(/\r\n/g, "\n").split("\n");
13558
+ const out = [];
13559
+ let i = 0;
13560
+ const n = lines.length;
13561
+ const flushParagraph = (buffer) => {
13562
+ if (buffer.length === 0) return;
13563
+ out.push("<p>" + renderInline(buffer.join("\n")) + "</p>");
13564
+ buffer.length = 0;
13565
+ };
13566
+ let paragraph = [];
13567
+ while (i < n) {
13568
+ const line = lines[i];
13569
+ const fence = /^```([\w+-]*)\s*$/.exec(line);
13570
+ if (fence !== null) {
13571
+ flushParagraph(paragraph);
13572
+ const lang = fence[1] ?? "";
13573
+ i += 1;
13574
+ const code = [];
13575
+ while (i < n && !/^```\s*$/.test(lines[i])) {
13576
+ code.push(lines[i]);
13577
+ i += 1;
13578
+ }
13579
+ i += 1;
13580
+ const langAttr = lang === "" ? "" : " class=\"language-" + escapeHtml(lang) + "\"";
13581
+ out.push("<pre" + langAttr + "><code>" + escapeHtml(code.join("\n")) + "</code></pre>");
13582
+ continue;
13583
+ }
13584
+ const heading = /^(#{1,6})\s+(.*)$/.exec(line);
13585
+ if (heading !== null) {
13586
+ flushParagraph(paragraph);
13587
+ const level = heading[1].length;
13588
+ out.push("<h" + level + ">" + renderInline(heading[2] ?? "") + "</h" + level + ">");
13589
+ i += 1;
13590
+ continue;
13591
+ }
13592
+ if (/^\s*(---+|\*\*\*+|___+)\s*$/.test(line)) {
13593
+ flushParagraph(paragraph);
13594
+ out.push("<hr />");
13595
+ i += 1;
13596
+ continue;
13597
+ }
13598
+ if (line.includes("|") && i + 1 < n && /^\s*\|?[\s:|-]+\|?\s*$/.test(lines[i + 1]) && lines[i + 1].includes("-")) {
13599
+ flushParagraph(paragraph);
13600
+ const headerCells = splitTableRow(line);
13601
+ i += 2;
13602
+ const rows = [];
13603
+ while (i < n && lines[i].includes("|")) {
13604
+ rows.push(splitTableRow(lines[i]));
13605
+ i += 1;
13606
+ }
13607
+ out.push("<table>");
13608
+ out.push("<thead><tr>" + headerCells.map((cell) => "<th>" + renderInline(cell) + "</th>").join("") + "</tr></thead>");
13609
+ if (rows.length > 0) out.push("<tbody>" + rows.map((row) => "<tr>" + row.map((cell) => "<td>" + renderInline(cell) + "</td>").join("") + "</tr>").join("") + "</tbody>");
13610
+ out.push("</table>");
13611
+ continue;
13612
+ }
13613
+ if (/^>\s?(.*)$/.exec(line) !== null) {
13614
+ flushParagraph(paragraph);
13615
+ const body = [];
13616
+ while (i < n) {
13617
+ const q = /^>\s?(.*)$/.exec(lines[i]);
13618
+ if (q === null) break;
13619
+ body.push(q[1] ?? "");
13620
+ i += 1;
13621
+ }
13622
+ out.push("<blockquote><p>" + body.map((bodyLine) => renderInline(bodyLine)).join("<br />") + "</p></blockquote>");
13623
+ continue;
13624
+ }
13625
+ if (/^\s*([-*+])\s+(.*)$/.exec(line) !== null) {
13626
+ flushParagraph(paragraph);
13627
+ const items = [];
13628
+ while (i < n) {
13629
+ const item = /^\s*([-*+])\s+(.*)$/.exec(lines[i]);
13630
+ if (item === null) break;
13631
+ items.push("<li>" + renderInline(item[2] ?? "") + "</li>");
13632
+ i += 1;
13633
+ }
13634
+ out.push("<ul>" + items.join("") + "</ul>");
13635
+ continue;
13636
+ }
13637
+ if (/^\s*\d+[.)]\s+(.*)$/.exec(line) !== null) {
13638
+ flushParagraph(paragraph);
13639
+ const items = [];
13640
+ while (i < n) {
13641
+ const item = /^\s*\d+[.)]\s+(.*)$/.exec(lines[i]);
13642
+ if (item === null) break;
13643
+ items.push("<li>" + renderInline(item[1] ?? "") + "</li>");
13644
+ i += 1;
13645
+ }
13646
+ out.push("<ol>" + items.join("") + "</ol>");
13647
+ continue;
13648
+ }
13649
+ if (line.trim() === "") {
13650
+ flushParagraph(paragraph);
13651
+ i += 1;
13652
+ continue;
13653
+ }
13654
+ paragraph.push(line);
13655
+ i += 1;
13656
+ }
13657
+ flushParagraph(paragraph);
13658
+ return out.join("\n");
13659
+ }
13660
+ /** Split one table row into cells (respecting the leading/trailing pipes). */
13661
+ function splitTableRow(line) {
13662
+ const trimmed = line.trim();
13663
+ const inner = trimmed.startsWith("|") ? trimmed.slice(1) : trimmed;
13664
+ return (inner.endsWith("|") ? inner.slice(0, -1) : inner).split("|").map((cell) => cell.trim());
13665
+ }
13666
+ //#endregion
13400
13667
  //#region src/mobile/mobile-theme.ts
13401
13668
  const STORAGE_KEY = "dsh.remote.theme";
13402
13669
  const THEME_COLOR_META_NAME = "theme-color";
@@ -13549,6 +13816,26 @@ function ThemeToggle() {
13549
13816
  children: dark ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(SunIcon, {}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MoonIcon, {})
13550
13817
  });
13551
13818
  }
13819
+ /** localStorage key for the tool-call display toggle (persisted on the /m origin). */
13820
+ const SHOW_TOOL_CALLS_KEY = "dsh.mobile.showToolCalls";
13821
+ /** localStorage key for the injected-system-message display toggle. */
13822
+ const SHOW_SYSTEM_MESSAGES_KEY = "dsh.mobile.showSystemMessages";
13823
+ /** Read a boolean from localStorage defensively; falls back to the default. */
13824
+ function readStoredBoolean(key, fallback) {
13825
+ try {
13826
+ const raw = localStorage.getItem(key);
13827
+ if (raw === null) return fallback;
13828
+ return raw === "1" || raw.toLowerCase() === "true";
13829
+ } catch {
13830
+ return fallback;
13831
+ }
13832
+ }
13833
+ /** Persist a boolean toggle; storage failures are ignored (feature stays non-persistent). */
13834
+ function writeStoredBoolean(key, value) {
13835
+ try {
13836
+ localStorage.setItem(key, value ? "1" : "0");
13837
+ } catch {}
13838
+ }
13552
13839
  /** Extract the raw event from one history entry (the fold consumes events only). */
13553
13840
  function eventOf(entry) {
13554
13841
  return entry.event;
@@ -13624,6 +13911,10 @@ function ChatView({ session, mux, onBack }) {
13624
13911
  const [currentModel, setCurrentModel] = (0, import_react.useState)(void 0);
13625
13912
  /** Which bottom sheet is open. */
13626
13913
  const [sheet, setSheet] = (0, import_react.useState)(null);
13914
+ /** Show tool-call disclosures (default on, persisted on the /m origin). */
13915
+ const [showToolCalls, setShowToolCalls] = (0, import_react.useState)(() => readStoredBoolean(SHOW_TOOL_CALLS_KEY, true));
13916
+ /** Show injected system messages (default off, persisted on the /m origin). */
13917
+ const [showSystemMessages, setShowSystemMessages] = (0, import_react.useState)(() => readStoredBoolean(SHOW_SYSTEM_MESSAGES_KEY, false));
13627
13918
  /**
13628
13919
  * Composer preference from the plugin's host settings (default true keeps
13629
13920
  * the legacy Enter-to-send behavior until the preference loads).
@@ -13767,6 +14058,17 @@ function ChatView({ session, mux, onBack }) {
13767
14058
  ]);
13768
14059
  const modelLabel = currentModel?.model ?? "模型";
13769
14060
  const permissionLabel = permissions === void 0 ? void 0 : permissions.options.find((option) => option.value === permissions.currentValue)?.name ?? displayName(permissions.currentValue);
14061
+ const contextUsage = (0, import_react.useMemo)(() => {
14062
+ for (let index = messages.length - 1; index >= 0; index--) {
14063
+ const message = messages[index];
14064
+ const usage = message.usage;
14065
+ if (message.kind !== "assistant" || usage === void 0) continue;
14066
+ const window = message.contextWindow;
14067
+ if (window === void 0 || window <= 0) continue;
14068
+ const tokens = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0);
14069
+ return { pct: Math.round(tokens / window * 100) };
14070
+ }
14071
+ }, [messages]);
13770
14072
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
13771
14073
  className: "chat",
13772
14074
  children: [
@@ -13806,7 +14108,11 @@ function ChatView({ session, mux, onBack }) {
13806
14108
  },
13807
14109
  children: loading ? "加载中…" : "加载更早的消息"
13808
14110
  }),
13809
- messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageRow, { message }, message.id)),
14111
+ messages.map((message) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MessageRow, {
14112
+ message,
14113
+ showToolCalls,
14114
+ showSystemMessages
14115
+ }, message.id)),
13810
14116
  loading && messages.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
13811
14117
  className: "chat-typing",
13812
14118
  children: "加载中…"
@@ -13819,51 +14125,78 @@ function ChatView({ session, mux, onBack }) {
13819
14125
  }),
13820
14126
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
13821
14127
  className: "chat-tools",
13822
- children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
13823
- type: "button",
13824
- className: "chat-chip",
13825
- onClick: () => {
13826
- setSheet("model");
13827
- },
13828
- "aria-haspopup": "dialog",
13829
- children: [
13830
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
13831
- className: "chat-chip-label",
13832
- children: "模型"
13833
- }),
13834
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
13835
- className: "chat-chip-value",
13836
- children: modelLabel
13837
- }),
13838
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
13839
- className: "chat-chip-chevron",
13840
- "aria-hidden": true,
13841
- children: "›"
13842
- })
13843
- ]
13844
- }), permissionLabel !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
13845
- type: "button",
13846
- className: "chat-chip",
13847
- onClick: () => {
13848
- setSheet("permission");
13849
- },
13850
- "aria-haspopup": "dialog",
13851
- children: [
13852
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14128
+ children: [
14129
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
14130
+ type: "button",
14131
+ className: "chat-chip",
14132
+ onClick: () => {
14133
+ setSheet("model");
14134
+ },
14135
+ "aria-haspopup": "dialog",
14136
+ children: [
14137
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14138
+ className: "chat-chip-label",
14139
+ children: "模型"
14140
+ }),
14141
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14142
+ className: "chat-chip-value",
14143
+ children: modelLabel
14144
+ }),
14145
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14146
+ className: "chat-chip-chevron",
14147
+ "aria-hidden": true,
14148
+ children: "›"
14149
+ })
14150
+ ]
14151
+ }),
14152
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
14153
+ type: "button",
14154
+ className: "chat-chip",
14155
+ onClick: () => {
14156
+ setSheet("display");
14157
+ },
14158
+ "aria-haspopup": "dialog",
14159
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
13853
14160
  className: "chat-chip-label",
13854
- children: "权限"
13855
- }),
13856
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
13857
- className: "chat-chip-value",
13858
- children: permissionLabel
13859
- }),
13860
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14161
+ children: "显示"
14162
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
13861
14163
  className: "chat-chip-chevron",
13862
14164
  "aria-hidden": true,
13863
14165
  children: "›"
13864
- })
13865
- ]
13866
- })]
14166
+ })]
14167
+ }),
14168
+ permissionLabel !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("button", {
14169
+ type: "button",
14170
+ className: "chat-chip",
14171
+ onClick: () => {
14172
+ setSheet("permission");
14173
+ },
14174
+ "aria-haspopup": "dialog",
14175
+ children: [
14176
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14177
+ className: "chat-chip-label",
14178
+ children: "权限"
14179
+ }),
14180
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14181
+ className: "chat-chip-value",
14182
+ children: permissionLabel
14183
+ }),
14184
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14185
+ className: "chat-chip-chevron",
14186
+ "aria-hidden": true,
14187
+ children: "›"
14188
+ })
14189
+ ]
14190
+ }),
14191
+ contextUsage !== void 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14192
+ className: "chat-context" + (contextUsage.pct >= 80 ? " chat-context-warn" : ""),
14193
+ children: [
14194
+ "上下文 ",
14195
+ contextUsage.pct,
14196
+ "%"
14197
+ ]
14198
+ })
14199
+ ]
13867
14200
  }),
13868
14201
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
13869
14202
  className: "chat-inputbar",
@@ -13914,12 +14247,28 @@ function ChatView({ session, mux, onBack }) {
13914
14247
  onClose: () => {
13915
14248
  setSheet(null);
13916
14249
  }
14250
+ }),
14251
+ sheet === "display" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DisplaySheet, {
14252
+ showToolCalls,
14253
+ showSystemMessages,
14254
+ onToolCalls: (value) => {
14255
+ setShowToolCalls(value);
14256
+ writeStoredBoolean(SHOW_TOOL_CALLS_KEY, value);
14257
+ },
14258
+ onSystemMessages: (value) => {
14259
+ setShowSystemMessages(value);
14260
+ writeStoredBoolean(SHOW_SYSTEM_MESSAGES_KEY, value);
14261
+ },
14262
+ onClose: () => {
14263
+ setSheet(null);
14264
+ }
13917
14265
  })
13918
14266
  ]
13919
14267
  });
13920
14268
  }
13921
14269
  /** One rendered message row (user bubble or assistant bubble with folds). */
13922
- function MessageRow({ message }) {
14270
+ function MessageRow({ message, showToolCalls, showSystemMessages }) {
14271
+ if (message.kind === "user" && message.sourceKind !== void 0 && message.sourceKind !== "user" && !showSystemMessages) return null;
13923
14272
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
13924
14273
  className: `chat-msg chat-msg-${message.kind}${message.pending === true ? " chat-msg-pending" : ""}${message.failed === true ? " chat-msg-failed" : ""}`,
13925
14274
  children: [
@@ -13927,8 +14276,8 @@ function MessageRow({ message }) {
13927
14276
  text: message.reasoning,
13928
14277
  pending: message.pending === true
13929
14278
  }),
13930
- message.kind === "assistant" && message.tools !== void 0 && message.tools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolDisclosure, { tools: message.tools }),
13931
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollapsibleText, { text: message.text }),
14279
+ showToolCalls && message.kind === "assistant" && message.tools !== void 0 && message.tools.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ToolDisclosure, { tools: message.tools }),
14280
+ message.kind === "assistant" ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MarkdownText, { text: message.text }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(CollapsibleText, { text: message.text }),
13932
14281
  message.failed === true && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
13933
14282
  className: "chat-msg-failtag",
13934
14283
  children: "本次回复失败"
@@ -14022,6 +14371,32 @@ function ToolDisclosure({ tools }) {
14022
14371
  })]
14023
14372
  });
14024
14373
  }
14374
+ /**
14375
+ * Assistant text rendered as GFM markdown (escape-first, protocol
14376
+ * allow-list — see markdown.ts). Long replies collapse by clamping the
14377
+ * rendered block height instead of slicing the source, so half-cut code
14378
+ * fences or tables never leak malformed markup into the DOM. User
14379
+ * messages stay plain text (CollapsibleText).
14380
+ */
14381
+ function MarkdownText({ text }) {
14382
+ const [open, setOpen] = (0, import_react.useState)(false);
14383
+ const html = (0, import_react.useMemo)(() => renderMarkdown(text), [text]);
14384
+ const long = text.length > LONG_TEXT_LIMIT;
14385
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14386
+ className: "chat-msg-text chat-md" + (long && !open ? " chat-md-collapsed" : ""),
14387
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
14388
+ className: "chat-md-body",
14389
+ dangerouslySetInnerHTML: { __html: html }
14390
+ }), long && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
14391
+ type: "button",
14392
+ className: "chat-msg-toggle",
14393
+ onClick: () => {
14394
+ setOpen((value) => !value);
14395
+ },
14396
+ children: open ? "收起" : "展开全文(" + text.length + " 字)"
14397
+ })]
14398
+ });
14399
+ }
14025
14400
  /** Long assistant text collapses behind an explicit expand toggle. */
14026
14401
  function CollapsibleText({ text }) {
14027
14402
  const [open, setOpen] = (0, import_react.useState)(false);
@@ -14372,6 +14747,68 @@ function PermissionSheet({ sessionId, value, onChanged, onClose }) {
14372
14747
  })]
14373
14748
  });
14374
14749
  }
14750
+ /** The display-options sheet: tool calls and injected system messages toggles. */
14751
+ function DisplaySheet({ showToolCalls, showSystemMessages, onToolCalls, onSystemMessages, onClose }) {
14752
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Sheet, {
14753
+ title: "显示",
14754
+ onClose,
14755
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14756
+ role: "group",
14757
+ "aria-label": "显示选项",
14758
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14759
+ className: "sheet-toggle-row",
14760
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14761
+ className: "sheet-toggle-copy",
14762
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14763
+ className: "sheet-toggle-title",
14764
+ children: "工具调用"
14765
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14766
+ className: "sheet-toggle-desc",
14767
+ children: "显示助手使用的工具调用"
14768
+ })]
14769
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
14770
+ type: "button",
14771
+ role: "switch",
14772
+ "aria-label": "工具调用",
14773
+ "aria-checked": showToolCalls,
14774
+ className: `sheet-toggle-switch${showToolCalls ? " sheet-toggle-switch-on" : ""}`,
14775
+ onClick: () => {
14776
+ onToolCalls(!showToolCalls);
14777
+ },
14778
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14779
+ className: "sheet-toggle-switch-knob",
14780
+ "aria-hidden": true
14781
+ })
14782
+ })]
14783
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14784
+ className: "sheet-toggle-row",
14785
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
14786
+ className: "sheet-toggle-copy",
14787
+ children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14788
+ className: "sheet-toggle-title",
14789
+ children: "系统提示词"
14790
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14791
+ className: "sheet-toggle-desc",
14792
+ children: "显示注入到对话中的系统消息"
14793
+ })]
14794
+ }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("button", {
14795
+ type: "button",
14796
+ role: "switch",
14797
+ "aria-label": "系统提示词",
14798
+ "aria-checked": showSystemMessages,
14799
+ className: `sheet-toggle-switch${showSystemMessages ? " sheet-toggle-switch-on" : ""}`,
14800
+ onClick: () => {
14801
+ onSystemMessages(!showSystemMessages);
14802
+ },
14803
+ children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
14804
+ className: "sheet-toggle-switch-knob",
14805
+ "aria-hidden": true
14806
+ })
14807
+ })]
14808
+ })]
14809
+ })
14810
+ });
14811
+ }
14375
14812
  //#endregion
14376
14813
  //#region src/mobile/views/SessionListView.tsx
14377
14814
  /**
@@ -15206,6 +15643,85 @@ body {
15206
15643
  cursor: pointer;
15207
15644
  }
15208
15645
 
15646
+ /* Assistant markdown content (GFM subset; renderer in markdown.ts). */
15647
+ .chat-md {
15648
+ white-space: normal;
15649
+ }
15650
+ .chat-md-collapsed {
15651
+ max-height: 45vh;
15652
+ overflow: hidden;
15653
+ position: relative;
15654
+ }
15655
+ .chat-md-collapsed::after {
15656
+ content: '';
15657
+ position: absolute;
15658
+ left: 0;
15659
+ right: 0;
15660
+ bottom: 0;
15661
+ height: 28px;
15662
+ background: linear-gradient(transparent, var(--m-bg-raised));
15663
+ pointer-events: none;
15664
+ }
15665
+ .chat-md-body p { margin: 0 0 8px; }
15666
+ .chat-md-body p:last-child { margin-bottom: 0; }
15667
+ .chat-md-body h1, .chat-md-body h2, .chat-md-body h3,
15668
+ .chat-md-body h4, .chat-md-body h5, .chat-md-body h6 {
15669
+ margin: 12px 0 6px;
15670
+ font-weight: 650;
15671
+ line-height: 1.3;
15672
+ }
15673
+ .chat-md-body h1 { font-size: 1.35em; }
15674
+ .chat-md-body h2 { font-size: 1.25em; }
15675
+ .chat-md-body h3 { font-size: 1.15em; }
15676
+ .chat-md-body h4, .chat-md-body h5, .chat-md-body h6 { font-size: 1.05em; }
15677
+ .chat-md-body code {
15678
+ font-family: var(--m-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
15679
+ font-size: 0.9em;
15680
+ background: var(--m-accent-soft);
15681
+ padding: 1px 5px;
15682
+ border-radius: 4px;
15683
+ }
15684
+ .chat-md-body pre {
15685
+ margin: 8px 0;
15686
+ padding: 10px;
15687
+ background: #101726;
15688
+ color: #dbe4f5;
15689
+ border-radius: 8px;
15690
+ overflow-x: auto;
15691
+ font-size: 12.5px;
15692
+ line-height: 1.5;
15693
+ }
15694
+ .chat-md-body pre code {
15695
+ background: transparent;
15696
+ padding: 0;
15697
+ color: inherit;
15698
+ }
15699
+ .chat-md-body ul, .chat-md-body ol { margin: 4px 0 8px; padding-left: 22px; }
15700
+ .chat-md-body li { margin: 2px 0; }
15701
+ .chat-md-body blockquote {
15702
+ margin: 8px 0;
15703
+ padding: 4px 10px;
15704
+ border-left: 3px solid var(--m-accent);
15705
+ background: var(--m-accent-soft);
15706
+ border-radius: 0 6px 6px 0;
15707
+ color: var(--m-text-secondary);
15708
+ }
15709
+ .chat-md-body table {
15710
+ margin: 8px 0;
15711
+ border-collapse: collapse;
15712
+ display: block;
15713
+ overflow-x: auto;
15714
+ font-size: 13px;
15715
+ }
15716
+ .chat-md-body th, .chat-md-body td {
15717
+ border: 1px solid var(--m-border);
15718
+ padding: 4px 8px;
15719
+ }
15720
+ .chat-md-body th { background: var(--m-accent-soft); }
15721
+ .chat-md-body a { color: var(--m-accent); }
15722
+ .chat-md-body hr { border: none; border-top: 1px solid var(--m-border); margin: 10px 0; }
15723
+ .chat-md-body img { max-width: 100%; border-radius: 8px; }
15724
+
15209
15725
  /* ── message disclosures (reasoning / tools) ─────────────────────────── */
15210
15726
 
15211
15727
  .chat-disclosure {
@@ -15637,11 +16153,97 @@ body {
15637
16153
  .chat-disclosure-head:focus-visible,
15638
16154
  .sheet-option:focus-visible,
15639
16155
  .sheet-confirm-danger:focus-visible,
16156
+ .sheet-toggle-switch:focus-visible,
15640
16157
  .chat-input:focus-visible {
15641
16158
  outline: none;
15642
16159
  box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);
15643
16160
  }
15644
16161
 
16162
+ /* ── chat context usage chip (chat-tools row) ───────────────────────── */
16163
+
16164
+ .chat-context {
16165
+ flex: none;
16166
+ align-self: center;
16167
+ padding: 2px 10px;
16168
+ border: 1px solid var(--m-border);
16169
+ border-radius: 999px;
16170
+ background: color-mix(in srgb, var(--m-text-tertiary) 10%, transparent);
16171
+ color: var(--m-text-tertiary);
16172
+ font-size: 11px;
16173
+ line-height: 1.6;
16174
+ white-space: nowrap;
16175
+ }
16176
+
16177
+ .chat-context-warn {
16178
+ border-color: var(--m-danger);
16179
+ background: color-mix(in srgb, var(--m-danger) 14%, transparent);
16180
+ color: var(--m-danger);
16181
+ }
16182
+
16183
+ /* ── display sheet toggle rows ──────────────────────────────────────── */
16184
+
16185
+ .sheet-toggle-row {
16186
+ display: flex;
16187
+ align-items: center;
16188
+ gap: 12px;
16189
+ padding: 12px 8px;
16190
+ border-bottom: 1px solid var(--m-border);
16191
+ }
16192
+
16193
+ .sheet-toggle-row:last-child {
16194
+ border-bottom: none;
16195
+ }
16196
+
16197
+ .sheet-toggle-copy {
16198
+ flex: 1;
16199
+ min-width: 0;
16200
+ display: flex;
16201
+ flex-direction: column;
16202
+ gap: 2px;
16203
+ }
16204
+
16205
+ .sheet-toggle-title {
16206
+ font-size: 14px;
16207
+ }
16208
+
16209
+ .sheet-toggle-desc {
16210
+ color: var(--m-text-tertiary);
16211
+ font-size: 12px;
16212
+ line-height: 1.4;
16213
+ }
16214
+
16215
+ .sheet-toggle-switch {
16216
+ position: relative;
16217
+ flex: none;
16218
+ width: 44px;
16219
+ height: 26px;
16220
+ border: none;
16221
+ border-radius: 999px;
16222
+ background: var(--m-border);
16223
+ cursor: pointer;
16224
+ transition: background-color 0.15s ease;
16225
+ }
16226
+
16227
+ .sheet-toggle-switch-on {
16228
+ background: var(--m-accent);
16229
+ }
16230
+
16231
+ .sheet-toggle-switch-knob {
16232
+ position: absolute;
16233
+ top: 3px;
16234
+ left: 3px;
16235
+ width: 20px;
16236
+ height: 20px;
16237
+ border-radius: 50%;
16238
+ background: #fff;
16239
+ box-shadow: 0 1px 2px rgba(16, 24, 40, 0.2);
16240
+ transition: transform 0.15s ease;
16241
+ }
16242
+
16243
+ .sheet-toggle-switch-on .sheet-toggle-switch-knob {
16244
+ transform: translateX(18px);
16245
+ }
16246
+
15645
16247
  /* Accessibility: collapse every interaction/enter animation for
15646
16248
  reduced-motion users; surface layout itself stays static. */
15647
16249
  @media (prefers-reduced-motion: reduce) {
@@ -15661,6 +16263,8 @@ body {
15661
16263
  .sheet,
15662
16264
  .sheet-option,
15663
16265
  .sheet-confirm-danger,
16266
+ .sheet-toggle-switch,
16267
+ .sheet-toggle-switch-knob,
15664
16268
  .chat-input {
15665
16269
  animation: none;
15666
16270
  transition: none;