@rexvane/piweb 0.3.11 → 0.3.12

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 (170) hide show
  1. package/.next/BUILD_ID +1 -1
  2. package/.next/app-path-routes-manifest.json +6 -5
  3. package/.next/build-manifest.json +6 -6
  4. package/.next/prerender-manifest.json +7 -7
  5. package/.next/react-loadable-manifest.json +6 -6
  6. package/.next/routes-manifest.json +6 -0
  7. package/.next/server/app/_global-error/page.js +2 -2
  8. package/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  9. package/.next/server/app/_global-error.html +1 -1
  10. package/.next/server/app/_global-error.rsc +1 -1
  11. package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  12. package/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  13. package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  14. package/.next/server/app/_not-found/page.js +1 -1
  15. package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  16. package/.next/server/app/_not-found.html +1 -1
  17. package/.next/server/app/_not-found.rsc +1 -1
  18. package/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  20. package/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  21. package/.next/server/app/api/agent/[id]/events/route.js +2 -2
  22. package/.next/server/app/api/agent/[id]/route.js +1 -1
  23. package/.next/server/app/api/agent/[id]/tree/route.js +1 -1
  24. package/.next/server/app/api/agent/new/route.js +1 -1
  25. package/.next/server/app/api/config/route.js +1 -1
  26. package/.next/server/app/api/files/route.js +1 -1
  27. package/.next/server/app/api/git/route.js +2 -2
  28. package/.next/server/app/api/growth/route.js +1 -1
  29. package/.next/server/app/api/health/route.js +1 -1
  30. package/.next/server/app/api/models/route.js +1 -1
  31. package/.next/server/app/api/pi-settings/route.js +1 -1
  32. package/.next/server/app/api/plugins/route.js +1 -1
  33. package/.next/server/app/api/prompts/route.js +1 -0
  34. package/.next/server/app/api/prompts/route.js.nft.json +1 -0
  35. package/.next/server/app/api/prompts/route_client-reference-manifest.js +1 -0
  36. package/.next/server/app/api/security/route.js +1 -1
  37. package/.next/server/app/api/sessions/[id]/export/route.js +1 -1
  38. package/.next/server/app/api/sessions/[id]/route.js +1 -1
  39. package/.next/server/app/api/sessions/route.js +1 -1
  40. package/.next/server/app/api/skills/route.js +2 -2
  41. package/.next/server/app/api/update/route.js +2 -2
  42. package/.next/server/app/api/version/route.js +1 -1
  43. package/.next/server/app/api/web-auth/route.js +1 -1
  44. package/.next/server/app/api/workspaces/route.js +1 -1
  45. package/.next/server/app/icon.svg/route.js +1 -1
  46. package/.next/server/app/index.html +1 -1
  47. package/.next/server/app/index.rsc +3 -3
  48. package/.next/server/app/index.segments/__PAGE__.segment.rsc +3 -3
  49. package/.next/server/app/index.segments/_full.segment.rsc +3 -3
  50. package/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  51. package/.next/server/app/login/page.js +1 -1
  52. package/.next/server/app/login/page_client-reference-manifest.js +1 -1
  53. package/.next/server/app/login.html +1 -1
  54. package/.next/server/app/login.rsc +2 -2
  55. package/.next/server/app/login.segments/_full.segment.rsc +2 -2
  56. package/.next/server/app/login.segments/_tree.segment.rsc +1 -1
  57. package/.next/server/app/login.segments/login/__PAGE__.segment.rsc +2 -2
  58. package/.next/server/app/page.js +1 -1
  59. package/.next/server/app/page_client-reference-manifest.js +1 -1
  60. package/.next/server/app-paths-manifest.json +6 -5
  61. package/.next/server/chunks/496.js +1 -1
  62. package/.next/server/chunks/686.js +1 -1
  63. package/.next/server/middleware-build-manifest.js +1 -1
  64. package/.next/server/middleware-react-loadable-manifest.js +1 -1
  65. package/.next/server/pages/404.html +1 -1
  66. package/.next/server/pages/500.html +1 -1
  67. package/.next/server/server-reference-manifest.json +1 -1
  68. package/.next/static/2kt2UiWjv_viXpNlfjvBT/_buildManifest.js +1 -0
  69. package/.next/static/chunks/2155.2f27fe7098987699.js +1 -0
  70. package/.next/static/chunks/2608.ec8ff1f5767c9056.js +1 -0
  71. package/.next/static/chunks/2716.a9e808190a4e2424.js +1 -0
  72. package/.next/static/chunks/{794-d1a40dcf24d651d9.js → 3794-3679f1c05160977c.js} +1 -1
  73. package/.next/static/chunks/4127.dff5c3b33c6bfd1b.js +1 -0
  74. package/.next/static/chunks/4673.d3d0a7f2599080fb.js +1 -0
  75. package/.next/static/chunks/4686-5bbee099126af797.js +1 -0
  76. package/.next/static/chunks/4bd1b696-93bf506b236d6248.js +1 -0
  77. package/.next/static/chunks/app/_global-error/page-961c055baebb3872.js +1 -0
  78. package/.next/static/chunks/app/_not-found/page-2d40d1d7d916666a.js +1 -0
  79. package/.next/static/chunks/app/api/agent/[id]/events/route-961c055baebb3872.js +1 -0
  80. package/.next/static/chunks/app/api/agent/[id]/route-961c055baebb3872.js +1 -0
  81. package/.next/static/chunks/app/api/agent/[id]/tree/route-961c055baebb3872.js +1 -0
  82. package/.next/static/chunks/app/api/agent/new/route-961c055baebb3872.js +1 -0
  83. package/.next/static/chunks/app/api/config/route-961c055baebb3872.js +1 -0
  84. package/.next/static/chunks/app/api/files/route-961c055baebb3872.js +1 -0
  85. package/.next/static/chunks/app/api/git/route-961c055baebb3872.js +1 -0
  86. package/.next/static/chunks/app/api/growth/route-961c055baebb3872.js +1 -0
  87. package/.next/static/chunks/app/api/health/route-961c055baebb3872.js +1 -0
  88. package/.next/static/chunks/app/api/models/route-961c055baebb3872.js +1 -0
  89. package/.next/static/chunks/app/api/pi-settings/route-961c055baebb3872.js +1 -0
  90. package/.next/static/chunks/app/api/plugins/route-961c055baebb3872.js +1 -0
  91. package/.next/static/chunks/app/api/prompts/route-961c055baebb3872.js +1 -0
  92. package/.next/static/chunks/app/api/security/route-961c055baebb3872.js +1 -0
  93. package/.next/static/chunks/app/api/sessions/[id]/export/route-961c055baebb3872.js +1 -0
  94. package/.next/static/chunks/app/api/sessions/[id]/route-961c055baebb3872.js +1 -0
  95. package/.next/static/chunks/app/api/sessions/route-961c055baebb3872.js +1 -0
  96. package/.next/static/chunks/app/api/skills/route-961c055baebb3872.js +1 -0
  97. package/.next/static/chunks/app/api/update/route-961c055baebb3872.js +1 -0
  98. package/.next/static/chunks/app/api/version/route-961c055baebb3872.js +1 -0
  99. package/.next/static/chunks/app/api/web-auth/route-961c055baebb3872.js +1 -0
  100. package/.next/static/chunks/app/api/workspaces/route-961c055baebb3872.js +1 -0
  101. package/.next/static/chunks/app/layout-50d06e2078d6ce76.js +1 -0
  102. package/.next/static/chunks/app/login/page-52ef4841cbd199a8.js +1 -0
  103. package/.next/static/chunks/app/page-00c8ebf40ec5eaca.js +1 -0
  104. package/.next/static/chunks/framework-c4e651fe89edfa91.js +1 -0
  105. package/.next/static/chunks/{main-a315d7c58b599962.js → main-6159b22a8cb0b7c5.js} +2 -2
  106. package/.next/static/chunks/main-app-e9b53f0e1c6591b2.js +1 -0
  107. package/.next/static/chunks/next/dist/client/components/builtin/app-error-961c055baebb3872.js +1 -0
  108. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-961c055baebb3872.js +1 -0
  109. package/.next/static/chunks/next/dist/client/components/builtin/global-error-a7e260f8cc2582d7.js +1 -0
  110. package/.next/static/chunks/next/dist/client/components/builtin/not-found-961c055baebb3872.js +1 -0
  111. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-961c055baebb3872.js +1 -0
  112. package/.next/static/chunks/webpack-05a9e85529a18b93.js +1 -0
  113. package/.next/types/app/api/prompts/route.ts +353 -0
  114. package/.next/types/routes.d.ts +2 -1
  115. package/.next/types/validator.ts +9 -0
  116. package/package.json +1 -1
  117. package/src/app/api/prompts/route.ts +51 -0
  118. package/src/components/AppShell.tsx +42 -19
  119. package/src/components/ChatWindow.tsx +20 -35
  120. package/src/components/PromptPanel.tsx +257 -0
  121. package/src/components/SettingsPanel.tsx +59 -56
  122. package/src/i18n.tsx +38 -4
  123. package/src/lib/agent-manager.ts +35 -8
  124. package/src/lib/clipboard.ts +27 -0
  125. package/src/lib/command-validation.ts +1 -0
  126. package/src/lib/prompt-sources.ts +201 -0
  127. package/src/lib/types.ts +1 -0
  128. package/.next/static/222KLUIvefiGf7M0Ov-Qz/_buildManifest.js +0 -1
  129. package/.next/static/chunks/127.c31e688e156605e3.js +0 -1
  130. package/.next/static/chunks/155.a3c40ae481da1358.js +0 -1
  131. package/.next/static/chunks/4bd1b696-92152b0f5947070d.js +0 -1
  132. package/.next/static/chunks/608.5b8aae42a9998ee7.js +0 -1
  133. package/.next/static/chunks/673.93445c8600bc1dd9.js +0 -1
  134. package/.next/static/chunks/686-9f15e4e370d26815.js +0 -1
  135. package/.next/static/chunks/716.5266fe10b5578591.js +0 -1
  136. package/.next/static/chunks/app/_global-error/page-1dac1b3ca8adb975.js +0 -1
  137. package/.next/static/chunks/app/_not-found/page-9de30f6f614fce02.js +0 -1
  138. package/.next/static/chunks/app/api/agent/[id]/events/route-1dac1b3ca8adb975.js +0 -1
  139. package/.next/static/chunks/app/api/agent/[id]/route-1dac1b3ca8adb975.js +0 -1
  140. package/.next/static/chunks/app/api/agent/[id]/tree/route-1dac1b3ca8adb975.js +0 -1
  141. package/.next/static/chunks/app/api/agent/new/route-1dac1b3ca8adb975.js +0 -1
  142. package/.next/static/chunks/app/api/config/route-1dac1b3ca8adb975.js +0 -1
  143. package/.next/static/chunks/app/api/files/route-1dac1b3ca8adb975.js +0 -1
  144. package/.next/static/chunks/app/api/git/route-1dac1b3ca8adb975.js +0 -1
  145. package/.next/static/chunks/app/api/growth/route-1dac1b3ca8adb975.js +0 -1
  146. package/.next/static/chunks/app/api/health/route-1dac1b3ca8adb975.js +0 -1
  147. package/.next/static/chunks/app/api/models/route-1dac1b3ca8adb975.js +0 -1
  148. package/.next/static/chunks/app/api/pi-settings/route-1dac1b3ca8adb975.js +0 -1
  149. package/.next/static/chunks/app/api/plugins/route-1dac1b3ca8adb975.js +0 -1
  150. package/.next/static/chunks/app/api/security/route-1dac1b3ca8adb975.js +0 -1
  151. package/.next/static/chunks/app/api/sessions/[id]/export/route-1dac1b3ca8adb975.js +0 -1
  152. package/.next/static/chunks/app/api/sessions/[id]/route-1dac1b3ca8adb975.js +0 -1
  153. package/.next/static/chunks/app/api/sessions/route-1dac1b3ca8adb975.js +0 -1
  154. package/.next/static/chunks/app/api/skills/route-1dac1b3ca8adb975.js +0 -1
  155. package/.next/static/chunks/app/api/update/route-1dac1b3ca8adb975.js +0 -1
  156. package/.next/static/chunks/app/api/version/route-1dac1b3ca8adb975.js +0 -1
  157. package/.next/static/chunks/app/api/web-auth/route-1dac1b3ca8adb975.js +0 -1
  158. package/.next/static/chunks/app/api/workspaces/route-1dac1b3ca8adb975.js +0 -1
  159. package/.next/static/chunks/app/layout-eb62bcd99abe79f9.js +0 -1
  160. package/.next/static/chunks/app/login/page-d2308b48d013d1a6.js +0 -1
  161. package/.next/static/chunks/app/page-708cd3ee4cfe8af5.js +0 -1
  162. package/.next/static/chunks/framework-0f597d6e68ffafed.js +0 -1
  163. package/.next/static/chunks/main-app-1a92e20ec190a32b.js +0 -1
  164. package/.next/static/chunks/next/dist/client/components/builtin/app-error-1dac1b3ca8adb975.js +0 -1
  165. package/.next/static/chunks/next/dist/client/components/builtin/forbidden-1dac1b3ca8adb975.js +0 -1
  166. package/.next/static/chunks/next/dist/client/components/builtin/global-error-7bb5c4afcbc1772b.js +0 -1
  167. package/.next/static/chunks/next/dist/client/components/builtin/not-found-1dac1b3ca8adb975.js +0 -1
  168. package/.next/static/chunks/next/dist/client/components/builtin/unauthorized-1dac1b3ca8adb975.js +0 -1
  169. package/.next/static/chunks/webpack-9d84b382d4dec27a.js +0 -1
  170. /package/.next/static/{222KLUIvefiGf7M0Ov-Qz → 2kt2UiWjv_viXpNlfjvBT}/_ssgManifest.js +0 -0
@@ -28,6 +28,7 @@ import {
28
28
  } from "@/components/icons";
29
29
  import { DiffView, type DiffLine, parseUnifiedDiff } from "@/components/DiffView";
30
30
  import { languageForPath } from "@/lib/highlight";
31
+ import { copyText } from "@/lib/clipboard";
31
32
  import { OutlineRail } from "@/components/OutlineRail";
32
33
  import { useI18n } from "@/i18n";
33
34
  import type { ToolCardState } from "@/hooks/usePiWeb";
@@ -53,29 +54,6 @@ import {
53
54
  type Dict = Record<string, string>;
54
55
  type ToolCallContent = Extract<WebContent, { type: "toolCall" }>;
55
56
 
56
- async function copyText(text: string): Promise<boolean> {
57
- try {
58
- if (navigator.clipboard?.writeText) {
59
- await navigator.clipboard.writeText(text);
60
- return true;
61
- }
62
- } catch {
63
- // Non-secure local-network contexts need the legacy fallback below.
64
- }
65
- const field = document.createElement("textarea");
66
- field.value = text;
67
- field.style.position = "fixed";
68
- field.style.opacity = "0";
69
- document.body.appendChild(field);
70
- field.select();
71
- try {
72
- return document.execCommand("copy");
73
- } catch {
74
- return false;
75
- } finally {
76
- field.remove();
77
- }
78
- }
79
57
 
80
58
  /** dsh message-chrome formatRunDuration:整秒,分钟档秒数补零 */
81
59
  function formatRunDuration(ms: number, t: Dict): string {
@@ -112,17 +90,24 @@ const Markdown = memo(function Markdown({ text }: { text: string }) {
112
90
  );
113
91
  });
114
92
 
115
- /** 流式中的最后一段正文:纯文本逐字追加 + 闪烁光标,结束后再切 Markdown(避免每个 delta 重解析整段) */
116
- function StreamingText({ text }: { text: string }) {
117
- return (
118
- <div className="md">
119
- <p style={{ whiteSpace: "pre-wrap", wordBreak: "break-word", margin: 0 }}>
120
- {text}
121
- <span className="stream-cursor" aria-hidden />
122
- </p>
123
- </div>
124
- );
125
- }
93
+
94
+ /**
95
+ * 流式正文:边流边按 Markdown 渲染(标题/列表/代码块在生成过程中就成形)。
96
+ *
97
+ * 不是每个 delta 都重解析整段——那样会随回答变长线性变慢;这里按 ~100ms 节流,
98
+ * 只在最新文本稳定下来时才交给 Markdown,回合结束时外层会切到精确的最终渲染。
99
+ */
100
+ const STREAM_MARKDOWN_THROTTLE_MS = 100;
101
+
102
+ const StreamingMarkdown = memo(function StreamingMarkdown({ text }: { text: string }) {
103
+ const [shown, setShown] = useState(text);
104
+ useEffect(() => {
105
+ if (text === shown) return;
106
+ const timer = setTimeout(() => setShown(text), STREAM_MARKDOWN_THROTTLE_MS);
107
+ return () => clearTimeout(timer);
108
+ }, [text, shown]);
109
+ return <Markdown text={shown} />;
110
+ });
126
111
 
127
112
  /** dsh TurnTimePanel 的简化版:耗时胶囊常驻,点击弹层看本回合 token 用量 */
128
113
  function TurnMetaPill({ durationMs, usage, t }: { durationMs: number; usage?: TrajTokens & { cost?: { total?: number } }; t: Dict }) {
@@ -642,7 +627,7 @@ const ThinkStep = memo(function ThinkStep({ text, live, startedAt }: { text: str
642
627
  const NarrationBlock = memo(function NarrationBlock({ text, live, error }: { text: string; live?: boolean; error?: boolean }) {
643
628
  return (
644
629
  <div className="pw-narr" data-error={error || undefined}>
645
- {live ? <StreamingText text={text} /> : <Markdown text={text} />}
630
+ {live ? <StreamingMarkdown text={text} /> : <Markdown text={text} />}
646
631
  </div>
647
632
  );
648
633
  });
@@ -0,0 +1,257 @@
1
+ "use client";
2
+ /**
3
+ * 提示词面板:把"发给模型的提示词从哪来"集中列出来,点某一项查看原文。
4
+ *
5
+ * 只读:文件类来源的路径来自 Pi 资源加载器(项目记忆 / 系统提示词覆盖 / 追加提示词 /
6
+ * 技能 / 提示模板,含扩展与 npm 包提供的那些);热会话额外给出"组装后的系统提示词"
7
+ * 与每个工具的定义,它们不是文件、没有路径,直接在本面板内展开查看。
8
+ */
9
+ import { useCallback, useEffect, useState } from "react";
10
+ import { useI18n } from "@/i18n";
11
+ import { IconCopyOutline16, IconCheckOutline14, IconRefreshOutline14, IconCloseOutline14 } from "@/components/icons";
12
+ import { copyText } from "@/lib/clipboard";
13
+
14
+ export type PromptSourceKind = "system" | "append" | "agents" | "skill" | "template" | "tool" | "assembled";
15
+
16
+ export interface PromptSourceOrigin {
17
+ scope: "project" | "agent" | "package" | "extension";
18
+ name?: string;
19
+ }
20
+
21
+ export interface PromptSource {
22
+ kind: PromptSourceKind;
23
+ name: string;
24
+ path?: string;
25
+ origin: PromptSourceOrigin;
26
+ bytes?: number;
27
+ }
28
+
29
+ export interface PromptToolView {
30
+ name: string;
31
+ description?: string;
32
+ parameters?: unknown;
33
+ promptGuidelines?: string[];
34
+ source: "builtin" | "extension";
35
+ }
36
+
37
+ export interface PromptSourcesData {
38
+ sources: PromptSource[];
39
+ assembledSystemPrompt?: string;
40
+ tools?: PromptToolView[];
41
+ sessionReady: boolean;
42
+ }
43
+
44
+ const GROUP_KINDS: PromptSourceKind[] = ["system", "append", "agents", "skill", "template", "tool", "assembled"];
45
+
46
+ function formatBytes(bytes: number | undefined): string {
47
+ if (!bytes) return "";
48
+ if (bytes < 1024) return `${bytes} B`;
49
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
50
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
51
+ }
52
+
53
+ export function PromptPanel({
54
+ cwd,
55
+ sessionId,
56
+ refreshKey,
57
+ onOpenContent,
58
+ onClose,
59
+ }: {
60
+ cwd: string;
61
+ sessionId: string | null;
62
+ /** 变化即重新拉取(例如切换会话) */
63
+ refreshKey?: string | number | null;
64
+ /** 打开查看器显示内容(复用应用现有的文件查看器) */
65
+ onOpenContent?: (path: string, content: string) => void;
66
+ onClose: () => void;
67
+ }) {
68
+ const { t } = useI18n();
69
+ const [data, setData] = useState<PromptSourcesData | null>(null);
70
+ const [error, setError] = useState<string | null>(null);
71
+ const [busy, setBusy] = useState(false);
72
+ const [opening, setOpening] = useState<string | null>(null);
73
+ const [inline, setInline] = useState<{ title: string; content: string } | null>(null);
74
+ const [copied, setCopied] = useState(false);
75
+
76
+ const load = useCallback(async () => {
77
+ setBusy(true);
78
+ setError(null);
79
+ try {
80
+ const query = new URLSearchParams({ cwd });
81
+ if (sessionId) query.set("session", sessionId);
82
+ const response = await fetch(`/api/prompts?${query.toString()}`);
83
+ const body = await response.json();
84
+ if (!response.ok || !body.success) throw new Error(body.error || `request failed (${response.status})`);
85
+ setData(body.data as PromptSourcesData);
86
+ } catch (err) {
87
+ setError(err instanceof Error ? err.message : String(err));
88
+ } finally {
89
+ setBusy(false);
90
+ }
91
+ }, [cwd, sessionId]);
92
+
93
+ useEffect(() => {
94
+ void load();
95
+ }, [load, refreshKey]);
96
+
97
+ const originLabel = (origin: PromptSourceOrigin): string => {
98
+ if (origin.scope === "project") return t.promptOriginProject;
99
+ if (origin.scope === "agent") return t.promptOriginAgent;
100
+ if (origin.scope === "package") return `${t.promptOriginPackage} ${origin.name ?? ""}`.trim();
101
+ return `${t.promptOriginExtension} ${origin.name ?? ""}`.trim();
102
+ };
103
+
104
+ const kindLabel = (kind: PromptSourceKind): string => {
105
+ switch (kind) {
106
+ case "system": return t.promptKindSystem;
107
+ case "append": return t.promptKindAppend;
108
+ case "agents": return t.promptKindAgents;
109
+ case "skill": return t.promptKindSkill;
110
+ case "template": return t.promptKindTemplate;
111
+ case "tool": return t.promptKindTool;
112
+ case "assembled": return t.promptKindAssembled;
113
+ }
114
+ };
115
+
116
+ /** 文件类:按需读内容,交给外部查看器 */
117
+ const openFile = async (source: PromptSource) => {
118
+ if (!source.path || !onOpenContent) return;
119
+ setOpening(source.path);
120
+ setError(null);
121
+ try {
122
+ const response = await fetch("/api/prompts", {
123
+ method: "POST",
124
+ headers: { "Content-Type": "application/json" },
125
+ body: JSON.stringify({ action: "read", filePath: source.path, cwd }),
126
+ });
127
+ const body = await response.json();
128
+ if (!response.ok || !body.success) throw new Error(body.error || "read failed");
129
+ onOpenContent(source.path, body.data.content as string);
130
+ } catch (err) {
131
+ setError(err instanceof Error ? err.message : String(err));
132
+ } finally {
133
+ setOpening(null);
134
+ }
135
+ };
136
+
137
+ const groups = GROUP_KINDS.map((kind) => ({ kind, items: (data?.sources ?? []).filter((source) => source.kind === kind) })).filter(
138
+ (group) => group.items.length > 0,
139
+ );
140
+ const hasTools = (data?.tools?.length ?? 0) > 0;
141
+
142
+ return (
143
+ <div className="flex min-h-0 flex-1 flex-col">
144
+ <div className="hairline-b flex items-center gap-2 px-4 py-2.5">
145
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 13.5, fontWeight: 600 }}>{t.promptPanel}</span>
146
+ <button type="button" className="icon-btn" style={{ width: 26, height: 26 }} title={t.promptRefresh} aria-label={t.promptRefresh} onClick={() => void load()}>
147
+ <span className={busy ? "piweb-spin" : undefined} style={{ display: "inline-flex" }}>
148
+ <IconRefreshOutline14 size={14} />
149
+ </span>
150
+ </button>
151
+ <button type="button" className="icon-btn" style={{ width: 26, height: 26 }} title={t.close} aria-label={t.close} onClick={onClose}>
152
+ <IconCloseOutline14 size={14} />
153
+ </button>
154
+ </div>
155
+ <div className="min-h-0 flex-1 overflow-auto px-3 py-3" style={{ fontSize: 12.5 }}>
156
+ {error && (
157
+ <div className="mb-2 rounded-lg px-2 py-1.5" style={{ background: "var(--dsw-hover)", color: "var(--dsw-danger)" }} role="alert">
158
+ {error}
159
+ </div>
160
+ )}
161
+ {!data && busy && <div style={{ color: "var(--dsw-label-tertiary)" }}>{t.promptLoading}</div>}
162
+ {data && !data.sessionReady && (
163
+ <div className="mb-2 rounded-lg px-2 py-1.5" style={{ background: "var(--dsw-hover)", color: "var(--dsw-label-caption)" }} role="status">
164
+ {t.promptColdHint}
165
+ </div>
166
+ )}
167
+
168
+ {groups.map((group) => (
169
+ <section key={group.kind} className="mb-3">
170
+ <div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{kindLabel(group.kind)}</div>
171
+ <div className="flex flex-col gap-1">
172
+ {group.items.map((source) => (
173
+ <button
174
+ key={`${source.kind}:${source.path ?? source.name}`}
175
+ type="button"
176
+ className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left"
177
+ style={{ background: opening === source.path ? "var(--dsw-active)" : "transparent", border: "0.5px solid var(--dsw-border-l2)" }}
178
+ title={source.path ?? source.name}
179
+ onClick={() => void openFile(source)}
180
+ >
181
+ <span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-primary)" }}>{source.name}</span>
182
+ <span className="flex-none" style={{ fontSize: 10.5, color: "var(--dsw-label-tertiary)" }}>{formatBytes(source.bytes)}</span>
183
+ <span className="flex-none rounded-md px-1.5 py-0.5" style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}>
184
+ {originLabel(source.origin)}
185
+ </span>
186
+ </button>
187
+ ))}
188
+ </div>
189
+ </section>
190
+ ))}
191
+
192
+ {hasTools && (
193
+ <section className="mb-3">
194
+ <div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.promptKindTool}</div>
195
+ <div className="flex flex-col gap-1">
196
+ {data!.tools!.map((tool) => (
197
+ <button
198
+ key={`tool:${tool.name}`}
199
+ type="button"
200
+ className="flex w-full items-center gap-2 rounded-lg px-2 py-1.5 text-left"
201
+ style={{ border: "0.5px solid var(--dsw-border-l2)" }}
202
+ onClick={() => setInline({ title: `${tool.name}`, content: [tool.description ?? "", tool.promptGuidelines?.join("\n") ?? "", tool.parameters ? JSON.stringify(tool.parameters, null, 2) : ""].filter(Boolean).join("\n\n") })}
203
+ >
204
+ <span className="min-w-0 flex-1 truncate" style={{ color: "var(--dsw-label-primary)" }}>{tool.name}</span>
205
+ <span className="flex-none rounded-md px-1.5 py-0.5" style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}>
206
+ {tool.source === "builtin" ? t.promptToolBuiltin : t.promptToolExtension}
207
+ </span>
208
+ </button>
209
+ ))}
210
+ </div>
211
+ </section>
212
+ )}
213
+
214
+ {data?.assembledSystemPrompt && (
215
+ <section className="mb-3">
216
+ <div className="mb-1 px-1" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{t.promptKindAssembled}</div>
217
+ <button
218
+ type="button"
219
+ className="w-full rounded-lg px-2 py-1.5 text-left"
220
+ style={{ border: "0.5px solid var(--dsw-border-l2)" }}
221
+ onClick={() => setInline({ title: t.promptKindAssembled, content: data.assembledSystemPrompt ?? "" })}
222
+ >
223
+ <span className="truncate" style={{ color: "var(--dsw-label-primary)" }}>{t.promptAssembledKey}</span>
224
+ </button>
225
+ </section>
226
+ )}
227
+
228
+ {inline && (
229
+ <div className="mt-2">
230
+ <div className="mb-1 flex items-center gap-2">
231
+ <span className="min-w-0 flex-1 truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>{inline.title}</span>
232
+ <button
233
+ type="button"
234
+ className="icon-btn"
235
+ style={{ width: 24, height: 24 }}
236
+ title={copied ? t.copySuccess : t.copy}
237
+ aria-label={t.copy}
238
+ onClick={() => {
239
+ void copyText(inline.content).then((ok) => {
240
+ setCopied(ok);
241
+ setTimeout(() => setCopied(false), 1200);
242
+ });
243
+ }}
244
+ >
245
+ {copied ? <IconCheckOutline14 size={13} /> : <IconCopyOutline16 size={13} />}
246
+ </button>
247
+ <button type="button" className="icon-btn" style={{ width: 24, height: 24 }} title={t.close} aria-label={t.close} onClick={() => setInline(null)}>
248
+ <IconCloseOutline14 size={13} />
249
+ </button>
250
+ </div>
251
+ <pre className="pw-output" style={{ maxHeight: 420, overflow: "auto", margin: 0 }}>{inline.content}</pre>
252
+ </div>
253
+ )}
254
+ </div>
255
+ </div>
256
+ );
257
+ }
@@ -978,12 +978,12 @@ function ModelsSection() {
978
978
  return (
979
979
  <>
980
980
  <div className="flex items-center gap-2">
981
- {!usage && <span>{failed ? t.usageProbeFailed : busy ? t.queryingQuota : providerId === "xai" ? t.usageXaiDisclosure : t.usageReadOnlyDisclosure}</span>}
981
+ {!usage && <span>{failed ? t.usageProbeFailed : busy ? t.queryingQuota : t.usageReadOnlyDisclosure}</span>}
982
982
  {usage?.kind === "quota" && usage.planType && <span>{t.usagePlan} {titleCase(usage.planType)}</span>}
983
983
  {usage?.kind === "balance" && usage.primary && (
984
984
  <span>{usage.primaryKey === "used" ? t.usageBalanceUsed : t.usageQuotaRemaining} <span style={{ color: "var(--dsw-label-secondary)" }}>{fmtAmount(usage.primary)}</span></span>
985
985
  )}
986
- <button type="button" className="pw-chip" disabled={busy} title={providerId === "xai" ? t.usageXaiDisclosure : t.usageReadOnlyDisclosure} onClick={() => void fetchProviderUsage(providerId, true)}>
986
+ <button type="button" className="pw-chip" disabled={busy} title={t.usageReadOnlyDisclosure} onClick={() => void fetchProviderUsage(providerId, true)}>
987
987
  {busy ? t.usageProbing : usage ? t.usageRefresh : t.usageProbe}
988
988
  </button>
989
989
  </div>
@@ -1230,69 +1230,71 @@ function ModelsSection() {
1230
1230
  const editingThis = editing === p.id;
1231
1231
  return (
1232
1232
  <div key={p.id}>
1233
- <div className="flex items-center gap-3 rounded-2xl px-4 py-3" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
1234
- <ProviderBrand id={p.id} name={providerName(p.id)} size={34} />
1235
- <div className="min-w-0 flex-1">
1236
- <div className="truncate" style={{ fontSize: 14, fontWeight: 500 }}>{providerName(p.id)}</div>
1237
- <div className="truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
1238
- {p.id}{p.baseUrl ? ` · ${p.baseUrl}` : ""}
1233
+ {/* 卡片:身份/操作行在上,额度显示放在同一个框内的第二段(分隔线区分) */}
1234
+ <div className="flex flex-col rounded-2xl px-4 py-3" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
1235
+ <div className="flex items-center gap-3">
1236
+ <ProviderBrand id={p.id} name={providerName(p.id)} size={34} />
1237
+ <div className="min-w-0 flex-1">
1238
+ <div className="truncate" style={{ fontSize: 14, fontWeight: 500 }}>{providerName(p.id)}</div>
1239
+ <div className="truncate" style={{ fontSize: 11.5, color: "var(--dsw-label-caption)" }}>
1240
+ {p.id}{p.baseUrl ? ` · ${p.baseUrl}` : ""}
1241
+ </div>
1239
1242
  </div>
1240
- </div>
1241
- {isCustom && (
1243
+ {isCustom && (
1244
+ <span
1245
+ className="rounded-md px-1.5 py-0.5"
1246
+ style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
1247
+ >
1248
+ {t.customBadge}
1249
+ </span>
1250
+ )}
1251
+ <span
1252
+ className="inline-block h-2 w-2 rounded-full"
1253
+ style={{ background: p.authReady ? "var(--dsw-success)" : "var(--dsw-danger)" }}
1254
+ title={p.authError || (p.authReady ? t.providerReady : t.authUnavailable)}
1255
+ />
1242
1256
  <span
1243
1257
  className="rounded-md px-1.5 py-0.5"
1244
1258
  style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
1245
1259
  >
1246
- {t.customBadge}
1260
+ {t.credentialSource.replace("{source}", credentialSource(p))}
1247
1261
  </span>
1248
- )}
1249
- <span
1250
- className="inline-block h-2 w-2 rounded-full"
1251
- style={{ background: p.authReady ? "var(--dsw-success)" : "var(--dsw-danger)" }}
1252
- title={p.authError || (p.authReady ? t.providerReady : t.authUnavailable)}
1253
- />
1254
- <span
1255
- className="rounded-md px-1.5 py-0.5"
1256
- style={{ fontSize: 10.5, border: "0.5px solid var(--dsw-border-l3)", color: "var(--dsw-label-tertiary)" }}
1257
- >
1258
- {t.credentialSource.replace("{source}", credentialSource(p))}
1259
- </span>
1260
- <button className="btn-outline" style={{ height: 28, padding: "0 12px", fontSize: 12 }} onClick={() => startEdit(p.id)}>
1261
- {t.edit}
1262
- </button>
1263
- {p.storedAuthType === "oauth" && (
1264
- <button
1265
- style={{ height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-danger)" }}
1266
- onClick={async () => {
1267
- if (!window.confirm(t.confirmRemoveOAuth.replace("{name}", providerName(p.id)))) return;
1268
- await call({ action: "removeKey", providerId: p.id });
1269
- }}
1270
- >
1271
- {t.removeOAuth}
1262
+ <button className="btn-outline" style={{ height: 28, padding: "0 12px", fontSize: 12 }} onClick={() => startEdit(p.id)}>
1263
+ {t.edit}
1272
1264
  </button>
1273
- )}
1274
- {p.authTypes?.includes("oauth") && (
1275
- <button
1276
- className="btn-outline"
1277
- style={{ height: 28, padding: "0 12px", fontSize: 12 }}
1278
- onClick={() => {
1279
- setOauthDone(null);
1280
- void call({ action: "loginStart", providerId: p.id });
1281
- setOauthProvider(p.id);
1282
- }}
1283
- >
1284
- {t.oauthLogin}
1285
- </button>
1286
- )}
1287
- {customIds.has(p.id) && (
1288
- <button style={{ fontSize: 12.5, color: "var(--dsw-danger)" }} onClick={() => deleteCustom(p.id)}>
1289
- {t.delete}
1290
- </button>
1291
- )}
1265
+ {p.storedAuthType === "oauth" && (
1266
+ <button
1267
+ style={{ height: 28, padding: "0 12px", fontSize: 12, color: "var(--dsw-danger)" }}
1268
+ onClick={async () => {
1269
+ if (!window.confirm(t.confirmRemoveOAuth.replace("{name}", providerName(p.id)))) return;
1270
+ await call({ action: "removeKey", providerId: p.id });
1271
+ }}
1272
+ >
1273
+ {t.removeOAuth}
1274
+ </button>
1275
+ )}
1276
+ {p.authTypes?.includes("oauth") && (
1277
+ <button
1278
+ className="btn-outline"
1279
+ style={{ height: 28, padding: "0 12px", fontSize: 12 }}
1280
+ onClick={() => {
1281
+ setOauthDone(null);
1282
+ void call({ action: "loginStart", providerId: p.id });
1283
+ setOauthProvider(p.id);
1284
+ }}
1285
+ >
1286
+ {t.oauthLogin}
1287
+ </button>
1288
+ )}
1289
+ {customIds.has(p.id) && (
1290
+ <button style={{ fontSize: 12.5, color: "var(--dsw-danger)" }} onClick={() => deleteCustom(p.id)}>
1291
+ {t.delete}
1292
+ </button>
1293
+ )}
1292
1294
  </div>
1293
1295
 
1294
1296
  {p.id === "xai" && p.authReady && (
1295
- <div className="mt-1.5 flex items-center gap-2 px-4" style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
1297
+ <div className="mt-3 flex items-center gap-2 pt-3" style={{ borderTop: "0.5px solid var(--dsw-border-l2)", fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
1296
1298
  {xaiUsage ? (
1297
1299
  <span>{t.usageQuotaLabel} · {xaiUsage.model} · {t.usageRequests} {xaiUsage.requestRemaining.toLocaleString()}/{xaiUsage.requestLimit.toLocaleString()} · Tokens {fmtQuotaTokens(xaiUsage.tokenRemaining)}/{fmtQuotaTokens(xaiUsage.tokenLimit)}</span>
1298
1300
  ) : <span>{usageError ? t.usageProbeFailed : t.usageProbeDisclosure}</span>}
@@ -1303,10 +1305,11 @@ function ModelsSection() {
1303
1305
  )}
1304
1306
 
1305
1307
  {supportsUsageProbe(p.id) && p.id !== "xai" && p.authReady && (
1306
- <div className="mt-1.5 flex flex-col gap-1.5 px-4" style={{ fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
1308
+ <div className="mt-3 flex flex-col gap-1.5 pt-3" style={{ borderTop: "0.5px solid var(--dsw-border-l2)", fontSize: 11.5, color: "var(--dsw-label-tertiary)" }}>
1307
1309
  {renderProviderUsage(p.id)}
1308
1310
  </div>
1309
1311
  )}
1312
+ </div>
1310
1313
 
1311
1314
  {editingThis && (
1312
1315
  <div className="mt-2 rounded-2xl p-4" style={{ border: "0.5px solid var(--dsw-border-l2)" }}>
package/src/i18n.tsx CHANGED
@@ -21,7 +21,6 @@ const zh = {
21
21
  copyFailed: "复制失败",
22
22
  editMessage: "编辑这条消息",
23
23
  sendEdit: "重新发送",
24
- editWhileRunning: "会话正在运行,先停止再编辑",
25
24
  editMessageFailed: "编辑重发失败",
26
25
  export: "导出",
27
26
  settings: "设置",
@@ -214,7 +213,6 @@ const zh = {
214
213
  usageQuotaRemaining: "额度剩余",
215
214
  queryingQuota: "正在查询额度…",
216
215
  usageReadOnlyDisclosure: "读取提供商的只读用量端点,不消耗额度。",
217
- usageXaiDisclosure: "会向 xAI 发出一次 1 token 的请求以读取限流头,会计入用量。",
218
216
  usageReset: "刷新时间",
219
217
  usageWindowWeekly: "周限",
220
218
  usageWindowMonthly: "月限",
@@ -286,6 +284,25 @@ const zh = {
286
284
  contextSystem: "系统提示词",
287
285
  contextMessages: "对话消息",
288
286
  contextTools: "工具",
287
+ promptPanel: "提示词来源",
288
+ promptRefresh: "刷新",
289
+ promptLoading: "加载中…",
290
+ promptColdHint: "会话尚未启动:先列出磁盘上的提示词文件;打开会话后才会出现「组装后的系统提示词」与工具定义。",
291
+ promptOriginProject: "项目",
292
+ promptOriginAgent: "个人",
293
+ promptOriginPackage: "包",
294
+ promptOriginExtension: "扩展",
295
+ promptKindSystem: "系统提示词文件",
296
+ promptKindAppend: "追加提示词",
297
+ promptKindAgents: "项目记忆(AGENTS.md)",
298
+ promptKindSkill: "技能提示词",
299
+ promptKindTemplate: "提示模板",
300
+ promptKindTool: "工具提示词",
301
+ promptKindAssembled: "组装后的系统提示词",
302
+ promptAssembledKey: "当前生效的完整系统提示词",
303
+ promptToolBuiltin: "内置",
304
+ promptToolExtension: "扩展",
305
+ copySuccess: "已复制",
289
306
  contextSystemPrompt: "System Prompt",
290
307
  contextSystemTools: "System Tools",
291
308
  contextCustomTools: "Custom Tools",
@@ -579,7 +596,6 @@ const en: Dict = {
579
596
  copyFailed: "Copy failed",
580
597
  editMessage: "Edit this message",
581
598
  sendEdit: "Send again",
582
- editWhileRunning: "This session is running; stop it before editing",
583
599
  editMessageFailed: "Edit and resend failed",
584
600
  export: "Export",
585
601
  settings: "Settings",
@@ -771,7 +787,6 @@ const en: Dict = {
771
787
  usageQuotaRemaining: "Remaining quota",
772
788
  queryingQuota: "Querying quota…",
773
789
  usageReadOnlyDisclosure: "Reads the provider's read-only usage endpoint; consumes no quota.",
774
- usageXaiDisclosure: "Sends one 1-token request to xAI to read the rate-limit headers; counts against your usage.",
775
790
  usageReset: "Resets",
776
791
  usageWindowWeekly: "Weekly",
777
792
  usageWindowMonthly: "Monthly",
@@ -841,6 +856,25 @@ const en: Dict = {
841
856
  contextSystem: "System prompt",
842
857
  contextMessages: "Messages",
843
858
  contextTools: "Tools",
859
+ promptPanel: "Prompt sources",
860
+ promptRefresh: "Refresh",
861
+ promptLoading: "Loading…",
862
+ promptColdHint: "Session not started yet: disk files are listed now; the assembled system prompt and tool definitions appear once the session is running.",
863
+ promptOriginProject: "project",
864
+ promptOriginAgent: "user",
865
+ promptOriginPackage: "package",
866
+ promptOriginExtension: "extension",
867
+ promptKindSystem: "System prompt file",
868
+ promptKindAppend: "Appended prompt",
869
+ promptKindAgents: "Project memory (AGENTS.md)",
870
+ promptKindSkill: "Skill prompts",
871
+ promptKindTemplate: "Prompt templates",
872
+ promptKindTool: "Tool prompts",
873
+ promptKindAssembled: "Assembled system prompt",
874
+ promptAssembledKey: "Effective system prompt for this session",
875
+ promptToolBuiltin: "builtin",
876
+ promptToolExtension: "extension",
877
+ copySuccess: "Copied",
844
878
  contextSystemPrompt: "System Prompt",
845
879
  contextSystemTools: "System Tools",
846
880
  contextCustomTools: "Custom Tools",
@@ -684,17 +684,15 @@ async function buildContextBreakdown(
684
684
  // Memory:AGENTS.md 等注入文件(contextResources 的 project 部分)
685
685
  const memoryText = resources.filter((r) => r.source === "project").map((r) => r.content).join("\n\n");
686
686
  breakdown.memory = estimateTokensOf(memoryText);
687
- // Skills:从资源加载器取技能清单,按 system-prompt.js 的 formatSkillsForPrompt 段落定位
687
+ // Skills:从资源加载器取技能清单,按 SDK 的 formatSkillsForPrompt 段落定位。
688
+ // 该段落没有 Markdown 标题(原文以 "The following skills provide specialized instructions…" 开头,
689
+ // 结尾是 </available_skills>),以前按 "## Skills" 找永远找不到,技能段一直是 0。
688
690
  try {
689
691
  const { skills } = session.resourceLoader.getSkills();
690
692
  if (skills.length && prompt) {
691
- const header = prompt.indexOf("## Skills");
692
- if (header >= 0) {
693
- // skills 段之后是工具列表段(## Available Tools 等);找不到就取到结尾
694
- const next = prompt.slice(header + 1).search(/^## /m);
695
- const section = next >= 0 ? prompt.slice(header, header + 1 + next) : prompt.slice(header);
696
- breakdown.skills = estimateTokensOf(section);
697
- }
693
+ const start = prompt.indexOf("The following skills provide specialized instructions");
694
+ const end = prompt.indexOf("</available_skills>");
695
+ if (start >= 0 && end > start) breakdown.skills = estimateTokensOf(prompt.slice(start, end + "</available_skills>".length));
698
696
  }
699
697
  } catch {
700
698
  /* loader 不可用则跳过 */
@@ -1442,6 +1440,35 @@ export async function execute(m: Managed, cmd: AgentCommand): Promise<CommandRes
1442
1440
  const r = await session.navigateTree(entryId);
1443
1441
  return { ok: !r?.cancelled, data: { cancelled: r?.cancelled === true, editorText: r?.editorText } };
1444
1442
  }
1443
+ case "editAndResend": {
1444
+ // 编辑用户消息并重发:先停在跑的回合(用户改口即表示不要这一轮的输出),
1445
+ // 再回到该消息之前用新文本重新提问。放在服务端串行做,避免客户端"先 abort 再发"的竞态。
1446
+ const entryId = String((cmd as any).entryId ?? "");
1447
+ const text = (cmd.text ?? "").trim();
1448
+ if (!entryId) return { ok: false, error: "missing entryId" };
1449
+ if (!text) return { ok: false, error: "empty prompt" };
1450
+ if (m.promptSubmitting || m.resourceReloading) return { ok: false, error: "session is busy accepting another command" };
1451
+ m.promptSubmitting = true;
1452
+ try {
1453
+ const session = await ensureSession(m);
1454
+ if (m.disposed) throw new Error("session is disposed");
1455
+ if (session.isStreaming || m.runActive) {
1456
+ await session.abort().catch(() => undefined);
1457
+ // 等 SDK 真正回到空闲(abort 是异步的;最多等 5 秒,超时就按当前状态继续)
1458
+ for (let i = 0; i < 50 && !m.disposed && (session.isStreaming || m.runActive); i += 1) {
1459
+ await new Promise((resolve) => setTimeout(resolve, 100));
1460
+ }
1461
+ }
1462
+ if (m.disposed) throw new Error("session is disposed");
1463
+ const moved = await session.navigateTree(entryId);
1464
+ if (moved?.cancelled) return { ok: false, error: "edit cancelled" };
1465
+ await growthOf(m).prepare().catch(() => undefined);
1466
+ await session.prompt(text);
1467
+ return { ok: true };
1468
+ } finally {
1469
+ m.promptSubmitting = false;
1470
+ }
1471
+ }
1445
1472
  case "rename": {
1446
1473
  const name = (cmd.text ?? "").trim();
1447
1474
  if (m.session) m.session.setSessionName(name);
@@ -0,0 +1,27 @@
1
+ /**
2
+ * 剪贴板工具:优先用异步 Clipboard API,非安全上下文(局域网 http)退回 textarea + execCommand。
3
+ * 从 ChatWindow 抽出来共用(消息操作行与提示词面板都用它)。
4
+ */
5
+ export async function copyText(text: string): Promise<boolean> {
6
+ try {
7
+ if (navigator.clipboard?.writeText) {
8
+ await navigator.clipboard.writeText(text);
9
+ return true;
10
+ }
11
+ } catch {
12
+ // Non-secure local-network contexts need the legacy fallback below.
13
+ }
14
+ const field = document.createElement("textarea");
15
+ field.value = text;
16
+ field.style.position = "fixed";
17
+ field.style.opacity = "0";
18
+ document.body.appendChild(field);
19
+ field.select();
20
+ try {
21
+ return document.execCommand("copy");
22
+ } catch {
23
+ return false;
24
+ } finally {
25
+ field.remove();
26
+ }
27
+ }