@wonanut/dsh-agent-lab 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,190 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Agent 实验室 · 真实模型客户端
3
+ * The browser half of the "真实 DeepSeek" mode. It talks to the bundle's
4
+ * Host routes (index.js), which call the model configured in this DSH
5
+ * profile through ctx.llm. The mini-agent loop, tools and memory stay in this
6
+ * plugin — only the model decision step is delegated to the real model.
7
+ * ───────────────────────────────────────────────────────────── */
8
+
9
+ const AL_MODEL_STATUS_PATH = '/agent-lab/status';
10
+ const AL_MODEL_COMPLETE_PATH = '/agent-lab/complete';
11
+
12
+ const AL_REAL_SYSTEM_SUFFIX =
13
+ '你现在以真实模型模式驱动一个教学用迷你 Agent。可用工具以请求里的 schema 为准:calculator(计算)、clock(时间)、' +
14
+ 'dice_roll(掷骰子)、random_pick(随机选择)、password_generate(生成密码)、unit_convert(单位换算)、' +
15
+ 'memory_write(写入长期记忆)、memory_search(检索长期记忆)。需要外部能力时必须调用工具,' +
16
+ '不要凭空编造工具结果;同一轮只调用一个工具。回答用简体中文、口语、简洁(一般不超过 120 字)。';
17
+
18
+ const AL_REAL_SYSTEM_SUFFIX_FALLBACK =
19
+ '真实模型暂时不可用。请先检查 DSH 设置里的 DeepSeek 账号登录状态,或刷新页面。';
20
+
21
+ async function alModelFetchJson(path, options) {
22
+ const init = options || {};
23
+ let response;
24
+ try {
25
+ response = await fetch(path, {
26
+ credentials: 'same-origin',
27
+ cache: 'no-store',
28
+ ...init,
29
+ headers: { accept: 'application/json', ...(init.headers || {}) },
30
+ });
31
+ } catch (error) {
32
+ throw new Error('无法连接本地 Host 路由:' + ((error && error.message) || '网络错误'));
33
+ }
34
+ let data = null;
35
+ try {
36
+ const raw = await response.text();
37
+ data = raw ? JSON.parse(raw) : {};
38
+ } catch (error) {
39
+ data = null;
40
+ }
41
+ if (!response.ok) {
42
+ const message = data && data.error && data.error.message ? data.error.message : 'HTTP ' + response.status;
43
+ const failure = new Error(message);
44
+ failure.code = data && data.error && data.error.code ? data.error.code : 'HTTP_' + response.status;
45
+ throw failure;
46
+ }
47
+ if (!data || data.ok !== true) {
48
+ const message = data && data.error && data.error.message ? data.error.message : 'Host 返回了无法识别的响应';
49
+ const failure = new Error(message);
50
+ failure.code = data && data.error && data.error.code ? data.error.code : 'BAD_RESPONSE';
51
+ throw failure;
52
+ }
53
+ return data;
54
+ }
55
+
56
+ /** Read the current DSH model selection + availability without exposing secrets. */
57
+ async function alRealModelStatus() {
58
+ const data = await alModelFetchJson(AL_MODEL_STATUS_PATH, { method: 'GET' });
59
+ return {
60
+ available: Boolean(data.ok && data.provider && data.model),
61
+ provider: data.provider || '',
62
+ model: data.model || '',
63
+ modelName: data.modelName || data.model || '',
64
+ reasoningEffort: data.reasoningEffort || '',
65
+ signedIn: data.signedIn,
66
+ models: Array.isArray(data.models) ? data.models : [],
67
+ contextWindow: Number.isFinite(data.contextWindow) ? data.contextWindow : null,
68
+ maxOutputTokens: Number.isFinite(data.maxOutputTokens) ? data.maxOutputTokens : null,
69
+ };
70
+ }
71
+
72
+ /** Human-readable token counts for the terminal footer (420 / 128K / 1M). */
73
+ function alFormatTokens(value) {
74
+ const n = Number(value);
75
+ if (!Number.isFinite(n) || n <= 0) return '—';
76
+ if (n >= 1000000) return (n / 1000000).toFixed(n % 1000000 === 0 ? 0 : 1) + 'M';
77
+ if (n >= 1000) return Math.round(n / 1000) + 'K';
78
+ return String(Math.round(n));
79
+ }
80
+
81
+ /**
82
+ * Convert the engine's context messages into the simple wire format the Host
83
+ * route expects. System messages fold into `system`; assistant tool-call and
84
+ * tool-result messages keep their call ids so the model sees a valid loop.
85
+ */
86
+ function alRealWireMessages(modelMessages) {
87
+ const systemParts = [];
88
+ const messages = [];
89
+ const list = Array.isArray(modelMessages) ? modelMessages : [];
90
+ for (let i = 0; i < list.length; i += 1) {
91
+ const message = list[i];
92
+ if (!message || typeof message !== 'object') continue;
93
+ if (message.role === 'system') {
94
+ if (typeof message.content === 'string' && message.content.trim()) systemParts.push(message.content.trim());
95
+ continue;
96
+ }
97
+ if (message.role === 'user') {
98
+ messages.push({ role: 'user', content: String(message.content || '') });
99
+ continue;
100
+ }
101
+ if (message.role === 'assistant') {
102
+ const toolCalls = [];
103
+ if (message.kind === 'tool_call' && message.toolCallId) {
104
+ let args = message.toolArgs;
105
+ if (typeof args !== 'string') args = JSON.stringify(args || {});
106
+ toolCalls.push({ id: message.toolCallId, name: message.toolName || '', args });
107
+ }
108
+ messages.push({ role: 'assistant', content: String(message.content || ''), toolCalls });
109
+ continue;
110
+ }
111
+ if (message.role === 'tool') {
112
+ messages.push({
113
+ role: 'tool',
114
+ content: String(message.content || ''),
115
+ toolCallId: message.toolCallId || '',
116
+ isError: String(message.content || '').indexOf('错误:') === 0,
117
+ });
118
+ }
119
+ }
120
+ return { system: systemParts.join('\n\n'), messages };
121
+ }
122
+
123
+ /**
124
+ * Build the async adapter the engine calls once per model step in real mode.
125
+ * Returns {type:'tool_call'|'final', ...}; the engine executes tools itself.
126
+ */
127
+ function alCreateRealModelAdapter(options) {
128
+ const settings = options || {};
129
+ return async function realModelAdapter(request) {
130
+ const wire = alRealWireMessages(request.messages);
131
+ const system = [wire.system, AL_REAL_SYSTEM_SUFFIX].filter(Boolean).join('\n\n');
132
+ const payload = {
133
+ system: system,
134
+ messages: wire.messages,
135
+ tools: request.tools || [],
136
+ /* 教学模式关闭 thinking:避免把 reasoning 块回传进下一轮工具循环,
137
+ * 让模型请求保持最小可解释,同时保留原生 tool-call 协议。 */
138
+ thinking: false,
139
+ maxTokens: 900,
140
+ temperature: 0.3,
141
+ };
142
+ const startedAt = Date.now();
143
+ const data = await alModelFetchJson(AL_MODEL_COMPLETE_PATH, {
144
+ method: 'POST',
145
+ headers: { 'content-type': 'application/json' },
146
+ body: JSON.stringify(payload),
147
+ });
148
+ if (typeof request.cancelled === 'function' && request.cancelled()) {
149
+ throw new Error('本轮已取消');
150
+ }
151
+ const latencyMs = typeof data.latencyMs === 'number' ? data.latencyMs : Math.max(0, Date.now() - startedAt);
152
+ const meta = {
153
+ provider: data.provider || '',
154
+ model: data.model || '',
155
+ latencyMs: latencyMs,
156
+ usage: data.usage || null,
157
+ };
158
+ if (Array.isArray(data.toolCalls) && data.toolCalls.length) {
159
+ const call = data.toolCalls[0];
160
+ let args = call.args;
161
+ if (typeof args === 'string') {
162
+ try {
163
+ args = JSON.parse(args);
164
+ } catch (error) {
165
+ args = {};
166
+ }
167
+ }
168
+ if (!args || typeof args !== 'object') args = {};
169
+ return {
170
+ type: 'tool_call',
171
+ name: String(call.name || ''),
172
+ args: args,
173
+ text: typeof data.text === 'string' ? data.text : '',
174
+ ...meta,
175
+ };
176
+ }
177
+ return {
178
+ type: 'final',
179
+ text: typeof data.text === 'string' && data.text.trim() ? data.text : '(真实模型没有返回内容)',
180
+ ...meta,
181
+ };
182
+ };
183
+ }
184
+
185
+ /** Describe the current mode for the terminal header. */
186
+ function alRealModeLabel(view, status) {
187
+ if (!view || view.modelMode !== 'real') return '模拟模型 · 离线';
188
+ const model = (view.model && (view.model.modelName || view.model.model)) || (status && (status.modelName || status.model)) || 'DeepSeek';
189
+ return '真实 · ' + model;
190
+ }
package/src/50-tui.js ADDED
@@ -0,0 +1,472 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ * Agent 实验室 · 模拟 TUI 终端
3
+ * A dark terminal-styled chat surface. Everything rendered here comes from
4
+ * lab.snapshot().transcript, so the terminal shows the same events the engine
5
+ * actually emits (context assembly, tool calls, memory writes, compaction).
6
+ * ───────────────────────────────────────────────────────────── */
7
+
8
+ const TUI_CSS = `
9
+ .al-tui-panel {
10
+ grid-area: tui;
11
+ --t-bg: #0b0e14;
12
+ --t-bg2: #0f1420;
13
+ --t-bg3: #151c2c;
14
+ --t-text: #d7dee9;
15
+ --t-dim: #7d8798;
16
+ --t-border: #1e2735;
17
+ --t-green: #7ee787;
18
+ --t-blue: #79c0ff;
19
+ --t-purple: #d2a8ff;
20
+ --t-amber: #ffa657;
21
+ --t-red: #ff7b72;
22
+ --t-cyan: #56d4dd;
23
+ --t-mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, "Liberation Mono", monospace;
24
+ height: 100%;
25
+ min-height: 0;
26
+ border-color: #1b2432;
27
+ background: var(--t-bg);
28
+ box-shadow: 0 24px 60px -34px rgba(3, 7, 18, .72), 0 0 0 1px rgba(255, 255, 255, .02) inset;
29
+ }
30
+ .al-tui-panel .al-panel-head {
31
+ background: linear-gradient(180deg, #111827, #0d1422);
32
+ border-bottom-color: var(--t-border);
33
+ min-height: 44px;
34
+ padding-left: 12px;
35
+ }
36
+ .al-term-dots { display: flex; align-items: center; gap: 5px; margin-right: 2px; }
37
+ .al-term-dots i { width: 9px; height: 9px; border-radius: 50%; display: block; }
38
+ .al-term-dots i:nth-child(1) { background: #ff5f57; }
39
+ .al-term-dots i:nth-child(2) { background: #febc2e; }
40
+ .al-term-dots i:nth-child(3) { background: #28c840; }
41
+ .al-term-title { font-family: var(--t-mono); font-size: 11.5px; color: #94a3b8; letter-spacing: .02em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
42
+ .al-term-title b { color: #e2e8f0; font-weight: 600; }
43
+ .al-term-headright { display: flex; align-items: center; gap: 8px; flex: none; }
44
+ .al-live {
45
+ display: inline-flex; align-items: center; gap: 5px; height: 22px; padding: 0 8px;
46
+ border-radius: 999px; border: 1px solid var(--t-border); background: #0e1524;
47
+ font-family: var(--t-mono); font-size: 10px; color: #94a3b8; white-space: nowrap;
48
+ }
49
+ .al-live i { width: 6px; height: 6px; border-radius: 50%; background: #64748b; display: block; }
50
+ .al-live.run i { background: #7ee787; box-shadow: 0 0 0 0 rgba(126, 231, 135, .55); animation: al-pulse 1.5s infinite; }
51
+ .al-live.err i { background: #ff7b72; }
52
+ @keyframes al-pulse {
53
+ 0% { box-shadow: 0 0 0 0 rgba(126, 231, 135, .5); }
54
+ 70% { box-shadow: 0 0 0 6px rgba(126, 231, 135, 0); }
55
+ 100% { box-shadow: 0 0 0 0 rgba(126, 231, 135, 0); }
56
+ }
57
+ .al-pace { display: inline-flex; align-items: center; gap: 1px; padding: 2px; border-radius: 7px; background: #0e1524; border: 1px solid var(--t-border); }
58
+ .al-pace button {
59
+ height: 18px; padding: 0 6px; border: 0; border-radius: 5px; background: transparent;
60
+ font-family: var(--t-mono); font-size: 9.5px; color: #7d8798; transition: background .15s ease, color .15s ease;
61
+ }
62
+ .al-pace button:hover { color: #d7dee9; }
63
+ .al-pace button.on { background: #1c2740; color: #d2a8ff; }
64
+ .al-term-tokens { font-family: var(--t-mono); font-size: 10px; color: #7d8798; white-space: nowrap; }
65
+ .al-term-tokens b { color: #79c0ff; font-weight: 600; }
66
+ .al-mode { display: inline-flex; align-items: center; gap: 1px; padding: 2px; border-radius: 7px; background: #0e1524; border: 1px solid var(--t-border); }
67
+ .al-mode button {
68
+ height: 18px; padding: 0 7px; border: 0; border-radius: 5px; background: transparent;
69
+ font-family: var(--t-mono); font-size: 9.5px; color: #7d8798; transition: background .15s ease, color .15s ease;
70
+ }
71
+ .al-mode button:hover { color: #d7dee9; }
72
+ .al-mode button.on { background: #1c2740; color: #7ee787; }
73
+ .al-mode button.on.real { color: #d2a8ff; }
74
+ .al-mode button:disabled { opacity: .38; cursor: not-allowed; }
75
+ .al-term-model {
76
+ display: inline-flex; align-items: center; gap: 4px; height: 20px; padding: 0 7px;
77
+ border-radius: 6px; border: 1px solid #26344a; background: #0e1524;
78
+ font-family: var(--t-mono); font-size: 9.5px; color: #8fa3bd; white-space: nowrap; max-width: 180px;
79
+ overflow: hidden; text-overflow: ellipsis;
80
+ }
81
+ .al-term-model.real { color: #d2a8ff; border-color: #3b3060; background: #17122a; }
82
+ .al-term-model.warn { color: #ffa657; border-color: #4a3520; background: #231a10; }
83
+
84
+ .al-term-log {
85
+ flex: 1; min-height: 0; overflow-y: auto; overflow-x: hidden;
86
+ padding: 14px 16px 18px;
87
+ background:
88
+ radial-gradient(900px 220px at 50% 0%, rgba(56, 102, 255, .07), transparent 70%),
89
+ var(--t-bg);
90
+ scroll-behavior: smooth;
91
+ }
92
+ .al-term-log::-webkit-scrollbar { width: 9px; }
93
+ .al-term-log::-webkit-scrollbar-thumb { background: #243044; border: 3px solid transparent; border-radius: 999px; background-clip: content-box; }
94
+ .al-term-log::-webkit-scrollbar-thumb:hover { background: #3a4a66; background-clip: content-box; }
95
+
96
+ .al-term-line {
97
+ display: flex; gap: 8px; align-items: flex-start;
98
+ padding: 2px 0; font-family: var(--t-mono); font-size: 12.5px; line-height: 1.7;
99
+ color: var(--t-text);
100
+ }
101
+ .al-term-prefix { flex: none; width: 15px; text-align: center; color: var(--t-dim); font-size: 11.5px; user-select: none; padding-top: 1px; }
102
+ .al-term-text { flex: 1; min-width: 0; white-space: pre-wrap; word-break: break-word; }
103
+ .al-ln-user .al-term-prefix { color: var(--t-green); }
104
+ .al-ln-user .al-term-text { color: #f0f4fa; font-weight: 520; }
105
+ .al-ln-banner { margin: 0 0 10px; padding: 9px 11px; border: 1px solid #1d2b40; border-left: 3px solid #7ee787; border-radius: 8px; background: #0d1725; }
106
+ .al-ln-banner .al-term-prefix { color: var(--t-green); }
107
+ .al-ln-banner .al-term-text { color: #9fb0c7; font-size: 11.5px; line-height: 1.65; }
108
+ .al-ln-meta { padding: 1px 0; }
109
+ .al-ln-meta .al-term-prefix, .al-ln-meta .al-term-text { color: #66748a; font-size: 11px; }
110
+ .al-ln-info .al-term-prefix, .al-ln-info .al-term-text { color: #7d8798; font-size: 11.5px; }
111
+ .al-ln-llm .al-term-prefix { color: var(--t-purple); }
112
+ .al-ln-llm .al-term-text { color: #e3d9ff; }
113
+ .al-ln-agent .al-term-prefix { color: var(--t-cyan); }
114
+ .al-ln-agent .al-term-text { color: #c9f1f4; }
115
+ .al-ln-tool .al-term-prefix, .al-ln-tool .al-term-text { color: var(--t-amber); }
116
+ .al-ln-toolresult .al-term-prefix, .al-ln-toolresult .al-term-text { color: #8fbf8f; }
117
+ .al-ln-memory .al-term-prefix, .al-ln-memory .al-term-text { color: var(--t-purple); }
118
+ .al-ln-compress .al-term-prefix, .al-ln-compress .al-term-text { color: #e5b567; }
119
+ .al-ln-error .al-term-prefix, .al-ln-error .al-term-text { color: var(--t-red); }
120
+ .al-ln-banner + .al-term-line { margin-top: 2px; }
121
+ .al-cursor {
122
+ display: inline-block; width: 7px; height: 14px; margin-left: 2px; vertical-align: -2px;
123
+ background: var(--t-purple); animation: al-blink 1s steps(2, start) infinite;
124
+ }
125
+ @keyframes al-blink { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
126
+
127
+ .al-suggest { margin: 12px 0 2px; display: grid; grid-template-columns: repeat(auto-fill, minmax(190px, 1fr)); gap: 7px; }
128
+ .al-suggest-btn {
129
+ display: flex; align-items: flex-start; gap: 8px; text-align: left;
130
+ padding: 9px 10px; border-radius: 9px; border: 1px solid #1c2738; background: #0d1420;
131
+ color: #aab8cc; font-size: 11.5px; line-height: 1.5; font-family: var(--t-mono);
132
+ transition: border-color .15s ease, background .15s ease, transform .15s ease, color .15s ease;
133
+ }
134
+ .al-suggest-btn:hover { border-color: #35507a; background: #111b2c; color: #dbe6f5; transform: translateY(-1px); }
135
+ .al-suggest-btn svg { flex: none; margin-top: 1px; color: #79c0ff; }
136
+ .al-suggest-head { grid-column: 1 / -1; display: flex; align-items: center; gap: 6px; margin-top: 2px; color: #66748a; font-family: var(--t-mono); font-size: 10.5px; letter-spacing: .02em; }
137
+ .al-term-empty { padding: 10px 2px 0; }
138
+
139
+ .al-term-foot { flex: none; border-top: 1px solid var(--t-border); background: #0c111c; }
140
+ .al-term-ctx {
141
+ display: flex; align-items: center; gap: 8px;
142
+ padding: 7px 13px 1px; cursor: pointer;
143
+ font-family: var(--t-mono); font-size: 10px; color: #7d8798;
144
+ transition: color .15s ease;
145
+ }
146
+ .al-term-ctx:hover { color: #d7dee9; }
147
+ .al-term-ctx-label { color: #8b98ab; white-space: nowrap; }
148
+ .al-term-ctx-bar {
149
+ flex: 1; height: 4px; min-width: 48px; border-radius: 999px;
150
+ background: #1b2433; overflow: hidden;
151
+ }
152
+ .al-term-ctx-bar i { display: block; height: 100%; border-radius: 999px; transition: width .25s ease, background .25s ease; }
153
+ .al-term-ctx-num { color: #aab8cc; white-space: nowrap; font-variant-numeric: tabular-nums; }
154
+ .al-term-ctx-window { color: #66748a; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 230px; }
155
+ @media (max-width: 640px) { .al-term-ctx-window { display: none; } }
156
+ .al-term-cmdbar { display: flex; align-items: center; gap: 5px; padding: 7px 12px 0; overflow-x: auto; scrollbar-width: none; }
157
+ .al-term-cmdbar::-webkit-scrollbar { display: none; }
158
+ .al-term-cmd {
159
+ display: inline-flex; align-items: center; gap: 4px; height: 23px; padding: 0 8px; flex: none;
160
+ border: 1px solid #1c2738; border-radius: 6px; background: transparent; color: #7d8798;
161
+ font-family: var(--t-mono); font-size: 10px; white-space: nowrap;
162
+ transition: color .15s ease, border-color .15s ease, background .15s ease;
163
+ }
164
+ .al-term-cmd:hover { color: #d7dee9; border-color: #33445f; background: #111a29; }
165
+ .al-term-cmd:disabled { opacity: .4; cursor: not-allowed; }
166
+ .al-term-inputrow { display: flex; align-items: flex-end; gap: 8px; padding: 7px 10px 4px 14px; }
167
+ .al-term-prompt { flex: none; color: var(--t-green); font-family: var(--t-mono); font-size: 13px; line-height: 26px; user-select: none; }
168
+ .al-term-textarea {
169
+ flex: 1; min-width: 0; resize: none; border: 0; outline: 0; background: transparent;
170
+ color: #eef3fb; font-family: var(--t-mono); font-size: 12.5px; line-height: 1.7;
171
+ padding: 4px 0 3px; max-height: 108px; min-height: 26px;
172
+ }
173
+ .al-term-textarea::placeholder { color: #55637a; }
174
+ .al-term-send {
175
+ flex: none; width: 30px; height: 30px; display: grid; place-items: center;
176
+ border: 1px solid #2b3a55; border-radius: 8px; background: #16233a; color: #9fc0ff;
177
+ transition: background .15s ease, border-color .15s ease, color .15s ease, opacity .15s ease;
178
+ }
179
+ .al-term-send:hover { background: #1d2f4d; border-color: #3d5680; color: #d7e6ff; }
180
+ .al-term-send:disabled { opacity: .35; cursor: not-allowed; }
181
+ .al-term-hint { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 0 13px 9px; }
182
+ .al-term-hint span { color: #566178; font-family: var(--t-mono); font-size: 10px; }
183
+ .al-term-hint .al-kbd { background: #0f1726; border-color: #263349; color: #8494ab; box-shadow: none; height: 16px; font-size: 9px; }
184
+ .al-term-hint-left { display: flex; align-items: center; gap: 5px; flex-wrap: wrap; }
185
+ @media (max-width: 640px) {
186
+ .al-term-title { display: none; }
187
+ .al-term-tokens { display: none; }
188
+ }
189
+ @media (max-width: 1180px) {
190
+ .al-tui-panel { height: 560px; min-height: 560px; }
191
+ }
192
+ @media (max-width: 720px) {
193
+ .al-tui-panel { height: 520px; min-height: 520px; }
194
+ .al-term-hint { padding-bottom: 7px; }
195
+ }
196
+ `;
197
+
198
+ const TUI_SUGGESTIONS = [
199
+ { icon: 'bolt', text: '算一下 (12+30)*3,然后记住结果' },
200
+ { icon: 'memory', text: '记住 我的项目代号是 Orion' },
201
+ { icon: 'search', text: '我的项目代号是什么?' },
202
+ { icon: 'clock', text: '现在几点?记下来' },
203
+ { icon: 'tools', text: '你有哪些工具?' },
204
+ { icon: 'cpu', text: '讲讲你是怎么工作的' },
205
+ { icon: 'spark', text: '掷两个 20 面骰子' },
206
+ { icon: 'smile', text: '从 火锅、烧烤、日料 里帮我选一个' },
207
+ ];
208
+
209
+ function TermLine(props) {
210
+ const line = props.line;
211
+ const kind = line.kind || 'info';
212
+ const prefix = kind === 'user' ? '›'
213
+ : kind === 'llm' ? '◆'
214
+ : kind === 'agent' ? '●'
215
+ : kind === 'tool' ? '⚙'
216
+ : kind === 'toolresult' ? '←'
217
+ : kind === 'memory' ? '◈'
218
+ : kind === 'compress' ? '⇣'
219
+ : kind === 'error' ? '✕'
220
+ : kind === 'banner' ? '▌'
221
+ : kind === 'meta' ? '·' : '·';
222
+ return e(
223
+ 'div',
224
+ { className: 'al-term-line al-ln-' + kind },
225
+ e('span', { className: 'al-term-prefix' }, prefix),
226
+ e(
227
+ 'span',
228
+ { className: 'al-term-text' },
229
+ line.text,
230
+ line.streaming ? e('span', { className: 'al-cursor' }) : null,
231
+ ),
232
+ );
233
+ }
234
+
235
+ function TuiPanel(props) {
236
+ const view = props.view;
237
+ const pace = view.pace || 'normal';
238
+ const [draft, setDraft] = useState('');
239
+ const logRef = useRef(null);
240
+ const areaRef = useRef(null);
241
+ const stickRef = useRef(true);
242
+ const transcript = view.transcript || [];
243
+ const busy = Boolean(view.busy);
244
+ const hasUser = transcript.some((line) => line.kind === 'user');
245
+ const memCount = (view.memory || []).length;
246
+ const used = view.context ? view.context.used : 0;
247
+ const limit = view.context ? view.context.limit : 0;
248
+ const realMode = view.modelMode === 'real';
249
+ const realAvailable = Boolean(props.realStatus && props.realStatus.available);
250
+ const realUnavailableReason = props.realStatus && props.realStatus.error ? props.realStatus.error : '';
251
+ const contextPct = limit > 0 ? Math.min(100, Math.round((used / limit) * 100)) : 0;
252
+ const contextColor = contextPct > 90 ? '#ff7b72' : contextPct > 70 ? '#ffa657' : '#7ee787';
253
+ const realWindow = props.realStatus && props.realStatus.contextWindow;
254
+ const realModelName = (view.model && (view.model.modelName || view.model.model)) || (props.realStatus && (props.realStatus.modelName || props.realStatus.model)) || 'DeepSeek';
255
+ const realModeTitle = realMode
256
+ ? realAvailable
257
+ ? '真实 DeepSeek:' + realModelName + ' · 每轮消耗账号额度'
258
+ : '真实模式未连接:' + (realUnavailableReason || '请检查 DeepSeek 账号登录')
259
+ : '模拟模式:规则驱动、离线、可复现';
260
+
261
+ useEffect(() => {
262
+ const area = areaRef.current;
263
+ if (!area) return;
264
+ area.style.height = 'auto';
265
+ area.style.height = Math.min(108, Math.max(26, area.scrollHeight)) + 'px';
266
+ }, [draft]);
267
+
268
+ useEffect(() => {
269
+ const log = logRef.current;
270
+ if (!log || !stickRef.current) return;
271
+ log.scrollTop = log.scrollHeight;
272
+ }, [transcript.length, view.streaming && view.streaming.text]);
273
+
274
+ const onScroll = () => {
275
+ const log = logRef.current;
276
+ if (!log) return;
277
+ stickRef.current = log.scrollHeight - log.scrollTop - log.clientHeight < 90;
278
+ };
279
+
280
+ const submit = (text) => {
281
+ const value = String(text == null ? draft : text).trim();
282
+ if (!value || busy) return;
283
+ stickRef.current = true;
284
+ setDraft('');
285
+ if (props.onSend) props.onSend(value);
286
+ };
287
+
288
+ const onKeyDown = (event) => {
289
+ if (event.key === 'Enter' && !event.shiftKey && !(event.nativeEvent && event.nativeEvent.isComposing)) {
290
+ event.preventDefault();
291
+ submit();
292
+ }
293
+ };
294
+
295
+ return e(
296
+ 'section',
297
+ { className: 'al-panel al-tui-panel', 'aria-label': 'Agent 实验室 TUI' },
298
+ e(
299
+ 'header',
300
+ { className: 'al-panel-head' },
301
+ e(
302
+ 'div',
303
+ { className: 'al-panel-title', style: { minWidth: 0 } },
304
+ e('span', { className: 'al-term-dots' }, e('i'), e('i'), e('i')),
305
+ e('span', { className: 'al-term-title' }, e('b', null, 'agent-lab'), ' — tui · ', props.activeTitle || '会话'),
306
+ ),
307
+ e(
308
+ 'div',
309
+ { className: 'al-term-headright' },
310
+ e(
311
+ 'span',
312
+ { className: 'al-term-model' + (view.modelMode === 'real' ? (props.realStatus && props.realStatus.available ? ' real' : ' warn') : ''), title: realModeTitle },
313
+ view.modelMode === 'real'
314
+ ? (props.realStatus && props.realStatus.available
315
+ ? '真实 · ' + ((view.model && (view.model.modelName || view.model.model)) || (props.realStatus.modelName || props.realStatus.model) || 'DeepSeek')
316
+ : '真实模式 · 未连接')
317
+ : '离线模拟',
318
+ ),
319
+ e(
320
+ 'span',
321
+ { className: 'al-mode', role: 'group', 'aria-label': '模型模式' },
322
+ e(
323
+ 'button',
324
+ { type: 'button', className: realMode ? '' : 'on', disabled: busy, onClick: () => props.onMode && props.onMode('sim'), title: '本地规则模拟:离线、可复现、不消耗额度' },
325
+ '模拟',
326
+ ),
327
+ e(
328
+ 'button',
329
+ {
330
+ type: 'button',
331
+ className: 'real' + (realMode ? ' on' : ''),
332
+ disabled: busy || !realAvailable,
333
+ onClick: () => props.onMode && props.onMode('real'),
334
+ title: realAvailable ? '调用 DSH 当前模型:' + (props.realStatus.modelName || props.realStatus.model) + '(消耗账号额度)' : '真实模型不可用:' + (realUnavailableReason || '请检查 DeepSeek 登录'),
335
+ },
336
+ '真实',
337
+ ),
338
+ ),
339
+ e('span', { className: 'al-pace', role: 'group', 'aria-label': '模拟速度' },
340
+ ['slow', 'normal', 'fast'].map((item) =>
341
+ e(
342
+ 'button',
343
+ {
344
+ key: item,
345
+ type: 'button',
346
+ className: pace === item ? 'on' : '',
347
+ onClick: () => props.onPace && props.onPace(item),
348
+ title: { slow: '慢速演示', normal: '正常速度', fast: '快速' }[item],
349
+ },
350
+ { slow: '慢', normal: '中', fast: '快' }[item],
351
+ ),
352
+ ),
353
+ ),
354
+ e(
355
+ 'span',
356
+ { className: 'al-live' + (busy ? ' run' : '') },
357
+ e('i'),
358
+ busy ? '运行中' : '就绪',
359
+ ),
360
+ ),
361
+ ),
362
+ e(
363
+ 'div',
364
+ { className: 'al-term-log', ref: logRef, onScroll, role: 'log', 'aria-live': 'polite' },
365
+ transcript.map((line) => e(TermLine, { key: line.id, line })),
366
+ !hasUser
367
+ ? e(
368
+ 'div',
369
+ { className: 'al-term-empty' },
370
+ e('div', { className: 'al-suggest-head' }, e(Icon, { name: 'wand', size: 12 }), '试试这些输入,观察右侧的流转'),
371
+ e(
372
+ 'div',
373
+ { className: 'al-suggest' },
374
+ TUI_SUGGESTIONS.map((item) =>
375
+ e(
376
+ 'button',
377
+ { key: item.text, type: 'button', className: 'al-suggest-btn', onClick: () => submit(item.text), disabled: busy },
378
+ e(Icon, { name: item.icon, size: 13 }),
379
+ e('span', null, item.text),
380
+ ),
381
+ ),
382
+ ),
383
+ )
384
+ : null,
385
+ ),
386
+ e(
387
+ 'footer',
388
+ { className: 'al-term-foot' },
389
+ e(
390
+ 'div',
391
+ {
392
+ className: 'al-term-ctx',
393
+ role: 'button',
394
+ tabIndex: 0,
395
+ onClick: () => props.onCommand && props.onCommand('context'),
396
+ title: '点击查看检查器「上下文」:教学预算 ' + limit + ' tok' + (realMode && realWindow ? ' · 真实模型窗口 ' + realWindow + ' tok' : ''),
397
+ },
398
+ e('span', { className: 'al-term-ctx-label' }, '上下文'),
399
+ e('span', { className: 'al-term-ctx-bar' }, e('i', { style: { width: contextPct + '%', background: contextColor } })),
400
+ e('span', { className: 'al-term-ctx-num' }, used + ' / ' + limit + ' tok · ' + contextPct + '%'),
401
+ e(
402
+ 'span',
403
+ { className: 'al-term-ctx-window' },
404
+ realMode && realWindow ? '真实窗口 ' + alFormatTokens(realWindow) : '教学预算 ' + alFormatTokens(limit) + ' · 超出自动压缩',
405
+ ),
406
+ ),
407
+ e(
408
+ 'div',
409
+ { className: 'al-term-cmdbar' },
410
+ e(
411
+ 'button',
412
+ { type: 'button', className: 'al-term-cmd', onClick: () => props.onCommand && props.onCommand('compact'), disabled: busy || (view.messages || []).length < 3, title: '手动压缩上下文' },
413
+ e(Icon, { name: 'compress', size: 11 }), '/compact',
414
+ ),
415
+ e(
416
+ 'button',
417
+ { type: 'button', className: 'al-term-cmd', onClick: () => props.onCommand && props.onCommand('new'), disabled: busy, title: '新建会话' },
418
+ e(Icon, { name: 'plus', size: 11 }), '/new',
419
+ ),
420
+ e(
421
+ 'button',
422
+ { type: 'button', className: 'al-term-cmd', onClick: () => props.onCommand && props.onCommand('memory'), title: '查看长期记忆' },
423
+ e(Icon, { name: 'memory', size: 11 }), '/memory · ' + memCount,
424
+ ),
425
+ e(
426
+ 'button',
427
+ { type: 'button', className: 'al-term-cmd', onClick: () => props.onCommand && props.onCommand('clear'), disabled: busy, title: '清空当前会话,开新会话' },
428
+ e(Icon, { name: 'refresh', size: 11 }), '/clear',
429
+ ),
430
+ e(
431
+ 'button',
432
+ { type: 'button', className: 'al-term-cmd', onClick: () => submit('/help'), disabled: busy, title: '查看命令' },
433
+ e(Icon, { name: 'info', size: 11 }), '/help',
434
+ ),
435
+ ),
436
+ e(
437
+ 'div',
438
+ { className: 'al-term-inputrow' },
439
+ e('span', { className: 'al-term-prompt' }, '›'),
440
+ e('textarea', {
441
+ ref: areaRef,
442
+ className: 'al-term-textarea',
443
+ value: draft,
444
+ rows: 1,
445
+ placeholder: busy ? 'Agent 正在运行,稍候…' : '输入消息,Enter 发送;试试「记住 我的项目代号是 Orion」',
446
+ spellCheck: false,
447
+ onChange: (event) => setDraft(event.target.value),
448
+ onKeyDown,
449
+ disabled: busy,
450
+ 'aria-label': '输入消息',
451
+ }),
452
+ e(
453
+ 'button',
454
+ { type: 'button', className: 'al-term-send', onClick: () => submit(), disabled: busy || !draft.trim(), title: '发送' },
455
+ e(Icon, { name: 'send', size: 15 }),
456
+ ),
457
+ ),
458
+ e(
459
+ 'div',
460
+ { className: 'al-term-hint' },
461
+ e(
462
+ 'div',
463
+ { className: 'al-term-hint-left' },
464
+ e('span', null, e('span', { className: 'al-kbd' }, 'Enter'), ' 发送 · '),
465
+ e('span', null, e('span', { className: 'al-kbd' }, 'Shift+Enter'), ' 换行 · '),
466
+ e('span', null, e('span', { className: 'al-kbd' }, '/'), ' 命令'),
467
+ ),
468
+ e('span', null, '对话与记忆保存在本机浏览器'),
469
+ ),
470
+ ),
471
+ );
472
+ }