@steerable/agent-cli 0.6.73 → 0.7.1

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,151 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ export function resolveProjectRequest(request) {
5
+ const projectArg = blankToUndefined(request.projectArg);
6
+ const cwdArg = blankToUndefined(request.cwdArg);
7
+ if (request.noProject && (projectArg || cwdArg)) {
8
+ return error('`--no-project` 不能与 `--project` 或 `--cwd` 同时使用。');
9
+ }
10
+ if (request.noProject)
11
+ return { kind: 'none' };
12
+ if (projectArg && cwdArg) {
13
+ const left = canonicalize(projectArg, request);
14
+ const right = canonicalize(cwdArg, request);
15
+ if (left.kind === 'error')
16
+ return left;
17
+ if (right.kind === 'error')
18
+ return right;
19
+ if (left.dir !== right.dir) {
20
+ return error(`--cwd 与 --project 指向不同目录:${left.dir}、${right.dir}。二者含义相同,请只保留一个。`);
21
+ }
22
+ }
23
+ const explicit = Boolean(projectArg || cwdArg);
24
+ const target = projectArg ?? cwdArg ?? request.cwd;
25
+ const canonical = canonicalize(target, request);
26
+ if (canonical.kind === 'error')
27
+ return canonical;
28
+ if (!explicit) {
29
+ const kind = protectedKind(canonical.dir, request);
30
+ if (kind) {
31
+ return {
32
+ kind: 'error',
33
+ code: 2,
34
+ reason: 'protected',
35
+ protectedKind: kind,
36
+ message: protectedRunMessage(kind),
37
+ };
38
+ }
39
+ }
40
+ return { kind: 'bind', dir: canonical.dir, explicit };
41
+ }
42
+ export function shortenProjectPath(folder) {
43
+ const parts = folder.split(/[/\\]/).filter(Boolean);
44
+ if (parts.length <= 2)
45
+ return folder;
46
+ return `…/${parts.slice(-2).join('/')}`;
47
+ }
48
+ export function projectStatusLabel(project) {
49
+ if (!project)
50
+ return '独立工作区';
51
+ return `项目 ${project.name} · ${shortenProjectPath(project.folderPath)}`;
52
+ }
53
+ export function protectedRunMessage(kind) {
54
+ return [
55
+ `不会自动把${kindLabel(kind)}绑定为项目:它包含你的全部文件。`,
56
+ '请改用:<product> run --project <目录> "…"',
57
+ '或使用独立工作区:<product> run --no-project "…"',
58
+ ].join('\n');
59
+ }
60
+ export function protectedTuiMessage(kind) {
61
+ return [
62
+ `不会自动把${kindLabel(kind)}绑定为项目:它包含你的全部文件。`,
63
+ '',
64
+ '请在项目目录里重新启动,或:',
65
+ ' <product> tui --project <目录>',
66
+ ' <product> tui --no-project 使用独立工作区',
67
+ ].join('\n');
68
+ }
69
+ function pathFor(platform) {
70
+ return platform === 'win32' ? path.win32 : path.posix;
71
+ }
72
+ function canonicalize(target, request) {
73
+ const absolute = pathFor(request.platform).resolve(request.cwd, target);
74
+ let dir;
75
+ try {
76
+ dir = request.realpath(absolute);
77
+ }
78
+ catch {
79
+ return error(`目录不存在:${absolute}`);
80
+ }
81
+ if (!request.isDirectory(dir))
82
+ return error(`不是目录:${dir}`);
83
+ return { kind: 'ok', dir };
84
+ }
85
+ function protectedKind(dir, request) {
86
+ const platformPath = pathFor(request.platform);
87
+ if (samePath(request.platform, dir, canonicalHome(request)))
88
+ return 'home';
89
+ if (samePath(request.platform, dir, platformPath.parse(dir).root))
90
+ return 'root';
91
+ const system = systemDirs(request.platform, request.systemRoot);
92
+ if (system.some((entry) => samePath(request.platform, entry, dir)))
93
+ return 'system';
94
+ return null;
95
+ }
96
+ function canonicalHome(request) {
97
+ const absolute = pathFor(request.platform).resolve(request.home);
98
+ try {
99
+ return request.realpath(absolute);
100
+ }
101
+ catch {
102
+ return absolute;
103
+ }
104
+ }
105
+ /** Windows paths that differ only by letter case are the same directory. */
106
+ function samePath(platform, left, right) {
107
+ if (platform === 'win32')
108
+ return left.toLowerCase() === right.toLowerCase();
109
+ return left === right;
110
+ }
111
+ function systemDirs(platform, systemRoot) {
112
+ if (platform === 'darwin')
113
+ return ['/System', '/usr', '/bin', '/sbin', '/private'];
114
+ if (platform === 'linux')
115
+ return ['/usr', '/etc', '/bin', '/sbin', '/var'];
116
+ if (platform === 'win32') {
117
+ const root = systemRoot?.trim() || process.env.SystemRoot || 'C:\\Windows';
118
+ return [path.win32.resolve(root)];
119
+ }
120
+ return [];
121
+ }
122
+ function kindLabel(kind) {
123
+ if (kind === 'home')
124
+ return '家目录';
125
+ if (kind === 'root')
126
+ return '根目录';
127
+ return '系统目录';
128
+ }
129
+ function error(message) {
130
+ return { kind: 'error', code: 2, message };
131
+ }
132
+ function blankToUndefined(value) {
133
+ const trimmed = value?.trim();
134
+ return trimmed ? trimmed : undefined;
135
+ }
136
+ export function defaultProjectFs() {
137
+ return {
138
+ home: os.homedir(),
139
+ platform: process.platform,
140
+ ...(process.env.SystemRoot ? { systemRoot: process.env.SystemRoot } : {}),
141
+ realpath: (target) => fs.realpathSync(target),
142
+ isDirectory: (target) => {
143
+ try {
144
+ return fs.statSync(target).isDirectory();
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ },
150
+ };
151
+ }
@@ -0,0 +1,8 @@
1
+ export interface RuleContext {
2
+ ruleFileCount?: number;
3
+ rulesActive?: boolean;
4
+ files?: string[];
5
+ project?: unknown;
6
+ }
7
+ /** Status line for loaded or unloaded project rule files. */
8
+ export declare function ruleStatusLine(context: RuleContext, surface: 'cli' | 'tui'): string | null;
package/dist/rules.js ADDED
@@ -0,0 +1,12 @@
1
+ /** Status line for loaded or unloaded project rule files. */
2
+ export function ruleStatusLine(context, surface) {
3
+ const count = context.ruleFileCount ?? 0;
4
+ if (count <= 0)
5
+ return null;
6
+ if (!context.rulesActive)
7
+ return `有 ${count} 个规则文件未加载(--trust 加载)`;
8
+ if (surface === 'tui')
9
+ return `已加载 ${count} 个规则文件`;
10
+ const names = (context.files ?? []).map((file) => file.split(/[/\\]/).pop() || file).filter((name) => name.length > 0);
11
+ return names.length > 0 ? `已加载 ${count} 个规则文件:${names.join('、')}` : `已加载 ${count} 个规则文件`;
12
+ }
package/dist/run.d.ts CHANGED
@@ -4,7 +4,7 @@ export interface RunRequest {
4
4
  task: string;
5
5
  chatId?: string;
6
6
  agentId?: string;
7
- cwd?: string;
7
+ projectId?: string;
8
8
  files: string[];
9
9
  policy: ApprovePolicy;
10
10
  json: boolean;
package/dist/run.js CHANGED
@@ -1,12 +1,15 @@
1
- import { decisionFor, denied } from './approve.js';
1
+ import { autoAnswer, decisionFor, denied } from './approve.js';
2
+ import { cliText } from './copy.js';
3
+ import { ruleStatusLine } from './rules.js';
2
4
  export async function runTurn(client, request, stdout, stderr, signal) {
3
5
  if (request.policy === 'allow-all') {
4
- stderr.write('warning: --approve allow-all allows every tool call in this run\n');
6
+ stderr.write(`${cliText('cli.approveAllWarning')}\n`);
5
7
  }
6
8
  let chatId = request.chatId;
7
9
  if (!chatId) {
8
10
  const created = await client.request('POST', '/api/v2/chats/new', {
9
11
  ...(request.agentId ? { agentId: request.agentId } : {}),
12
+ ...(request.projectId ? { projectId: request.projectId } : {}),
10
13
  });
11
14
  if (created.status !== 200) {
12
15
  stderr.write(`failed to create chat (${created.status})\n`);
@@ -18,21 +21,28 @@ export async function runTurn(client, request, stdout, stderr, signal) {
18
21
  return 1;
19
22
  }
20
23
  }
24
+ await reportRules(client, chatId, stderr);
21
25
  let exitCode = 0;
22
26
  let deniedApproval = false;
23
27
  const text = [];
24
- const approvals = watchApprovals(client, request.policy, () => {
28
+ const autoAnswered = [];
29
+ const printedTools = new Set();
30
+ const streamOrder = { heldTools: [], pendingAnswers: [] };
31
+ const spec = { policy: request.policy, answerAsk: request.policy === 'allow-all' };
32
+ const approvals = watchApprovals(client, spec, request, stdout, stderr, autoAnswered, streamOrder, () => {
25
33
  deniedApproval = true;
26
34
  });
27
35
  try {
28
36
  for await (const event of client.stream(`/api/v2/chats/${encodeURIComponent(chatId)}/send`, { message: messageText(request), ...(request.agentId ? { agentId: request.agentId } : {}) }, signal)) {
29
- const next = observeEvent(event, request, stdout, stderr, text);
37
+ const next = observeEvent(event, request, stdout, stderr, text, streamOrder, printedTools);
30
38
  if (next !== 0)
31
39
  exitCode = next;
32
40
  }
33
41
  }
34
42
  finally {
35
43
  approvals.stop();
44
+ if (request.streamJson)
45
+ flushStreamOrder(streamOrder, stdout);
36
46
  }
37
47
  if (signal.aborted && exitCode === 0)
38
48
  exitCode = 1;
@@ -44,16 +54,24 @@ export async function runTurn(client, request, stdout, stderr, signal) {
44
54
  stdout.write(`${JSON.stringify({
45
55
  ok: exitCode === 0,
46
56
  chatId,
57
+ ...(request.projectId ? { projectId: request.projectId } : {}),
47
58
  text: text.join(''),
48
59
  exitCode,
60
+ autoAnswered,
49
61
  })}\n`);
50
62
  }
51
63
  return exitCode;
52
64
  }
65
+ async function reportRules(client, chatId, stderr) {
66
+ const response = await client.request('GET', `/api/v2/chats/${encodeURIComponent(chatId)}/project-context`, undefined);
67
+ if (response.status !== 200)
68
+ return;
69
+ const line = ruleStatusLine(response.data, 'cli');
70
+ if (line)
71
+ stderr.write(`${line}\n`);
72
+ }
53
73
  function messageText(request) {
54
74
  const lines = [request.task];
55
- if (request.cwd)
56
- lines.push(`Working directory: ${request.cwd}`);
57
75
  if (request.files.length > 0) {
58
76
  lines.push('Files:');
59
77
  for (const file of request.files)
@@ -61,7 +79,155 @@ function messageText(request) {
61
79
  }
62
80
  return lines.join('\n');
63
81
  }
64
- function watchApprovals(client, policy, onDenied) {
82
+ function answerName(record) {
83
+ return record.kind === 'ask_user' ? 'ask_user' : record.toolName;
84
+ }
85
+ function writeAnswerBeforeTool(order, record, stdout, streamJson) {
86
+ if (!streamJson)
87
+ return;
88
+ const name = answerName(record);
89
+ const line = JSON.stringify({ type: 'auto_answered', ...record });
90
+ const index = order.heldTools.findIndex((tool) => tool.name === name || tool.names?.includes(name));
91
+ if (index < 0) {
92
+ order.pendingAnswers.push({ name, id: '', line });
93
+ return;
94
+ }
95
+ const tool = order.heldTools[index];
96
+ if (!tool)
97
+ return;
98
+ const waiting = (tool.names ?? [tool.name]).filter((item) => item !== name);
99
+ tool.prefix = [...(tool.prefix ?? []), line];
100
+ if (waiting.length > 0) {
101
+ tool.names = waiting;
102
+ tool.name = waiting[0] ?? '';
103
+ return;
104
+ }
105
+ order.heldTools.splice(index, 1);
106
+ for (const item of tool.prefix)
107
+ stdout.write(`${item}\n`);
108
+ stdout.write(`${tool.line}\n`);
109
+ }
110
+ function flushHeldActions(order, stdout) {
111
+ const kept = [];
112
+ for (const tool of order.heldTools) {
113
+ if (tool.kind !== 'actions') {
114
+ kept.push(tool);
115
+ continue;
116
+ }
117
+ for (const item of tool.prefix ?? [])
118
+ stdout.write(`${item}\n`);
119
+ stdout.write(`${tool.line}\n`);
120
+ }
121
+ order.heldTools = kept;
122
+ }
123
+ function unsettledToolNames(event) {
124
+ const actions = event.actions;
125
+ if (!Array.isArray(actions))
126
+ return [];
127
+ const names = [];
128
+ for (const action of actions) {
129
+ if (!action || typeof action !== 'object')
130
+ continue;
131
+ const row = action;
132
+ const settled = typeof row.durationMs === 'number' || typeof row.success === 'boolean' || row.error != null;
133
+ if (settled)
134
+ continue;
135
+ names.push(typeof row.tool === 'string' && row.tool ? row.tool : 'tool');
136
+ }
137
+ return names;
138
+ }
139
+ function eventField(event, key) {
140
+ const payload = event.payload;
141
+ if (!payload || typeof payload !== 'object')
142
+ return '';
143
+ const value = payload[key];
144
+ return typeof value === 'string' ? value : '';
145
+ }
146
+ function writeStreamJsonEvent(order, event, stdout) {
147
+ const line = JSON.stringify(event);
148
+ const actions = String(event.type) === 'executed_actions';
149
+ if (actions || event.type === 'content' || event.type === 'done')
150
+ flushHeldActions(order, stdout);
151
+ if (actions) {
152
+ const names = unsettledToolNames(event);
153
+ const matched = [];
154
+ for (const name of names) {
155
+ const index = order.pendingAnswers.findIndex((answer) => answer.name === name);
156
+ if (index < 0)
157
+ continue;
158
+ const answer = order.pendingAnswers.splice(index, 1)[0];
159
+ if (answer)
160
+ matched.push(answer.line);
161
+ }
162
+ if (matched.length > 0 || names.length === 0) {
163
+ for (const answer of matched)
164
+ stdout.write(`${answer}\n`);
165
+ stdout.write(`${line}\n`);
166
+ return;
167
+ }
168
+ order.heldTools.push({
169
+ name: names[0] ?? '',
170
+ id: '',
171
+ line,
172
+ kind: 'actions',
173
+ names,
174
+ });
175
+ return;
176
+ }
177
+ if (event.type === 'tool_call') {
178
+ const name = eventField(event, 'name');
179
+ const index = order.pendingAnswers.findIndex((answer) => answer.name === name);
180
+ if (index >= 0) {
181
+ const answer = order.pendingAnswers.splice(index, 1)[0];
182
+ stdout.write(`${answer.line}\n${line}\n`);
183
+ return;
184
+ }
185
+ order.heldTools.push({ name, id: eventField(event, 'id'), line, kind: 'call' });
186
+ return;
187
+ }
188
+ if (event.type === 'tool_result') {
189
+ const name = eventField(event, 'name');
190
+ const id = eventField(event, 'id');
191
+ const index = order.heldTools.findIndex((tool) => (id && tool.id === id) || (name && tool.name === name));
192
+ if (index >= 0) {
193
+ const tool = order.heldTools.splice(index, 1)[0];
194
+ stdout.write(`${tool.line}\n`);
195
+ }
196
+ }
197
+ stdout.write(`${line}\n`);
198
+ }
199
+ function writeHeld(tool, stdout, extra) {
200
+ for (const item of tool.prefix ?? [])
201
+ stdout.write(`${item}\n`);
202
+ if (extra)
203
+ stdout.write(`${extra}\n`);
204
+ stdout.write(`${tool.line}\n`);
205
+ }
206
+ function flushStreamOrder(order, stdout) {
207
+ const pending = order.pendingAnswers.splice(0);
208
+ const held = order.heldTools.splice(0);
209
+ const used = new Set();
210
+ for (const answer of pending) {
211
+ const index = held.findIndex((tool, toolIndex) => {
212
+ if (used.has(toolIndex))
213
+ return false;
214
+ return tool.name === answer.name || (tool.names?.includes(answer.name) ?? false);
215
+ });
216
+ if (index < 0) {
217
+ stdout.write(`${answer.line}\n`);
218
+ continue;
219
+ }
220
+ used.add(index);
221
+ const tool = held[index];
222
+ if (tool)
223
+ writeHeld(tool, stdout, answer.line);
224
+ }
225
+ held.forEach((tool, index) => {
226
+ if (!used.has(index))
227
+ writeHeld(tool, stdout);
228
+ });
229
+ }
230
+ function watchApprovals(client, spec, request, stdout, stderr, autoAnswered, streamOrder, onDenied) {
65
231
  let stopped = false;
66
232
  const loop = (async () => {
67
233
  for await (const event of client.events()) {
@@ -71,15 +237,38 @@ function watchApprovals(client, policy, onDenied) {
71
237
  const prompt = event.payload;
72
238
  if (!prompt.requestId)
73
239
  continue;
74
- const kind = decisionFor(policy, prompt);
240
+ const kind = decisionFor(spec, { mode: prompt.mode }, 'run');
241
+ if (kind === 'prompt')
242
+ continue;
75
243
  if (denied(kind))
76
244
  onDenied();
245
+ const record = {
246
+ kind: 'approval',
247
+ toolName: prompt.toolName ?? '',
248
+ category: prompt.category ?? '',
249
+ decision: kind,
250
+ };
251
+ autoAnswered.push(record);
252
+ writeAnswerBeforeTool(streamOrder, record, stdout, request.streamJson);
253
+ if (!request.json && !request.streamJson && record.toolName === 'network_egress' && kind === 'allow_once') {
254
+ stderr.write(`${egressProgressLine(prompt.arguments)}\n`);
255
+ }
77
256
  await client.decideApproval(prompt.requestId, kind);
78
257
  }
79
258
  else if (event.channel === 'ask-user:request') {
80
259
  const prompt = event.payload;
81
- if (prompt.requestId)
260
+ if (!prompt.requestId)
261
+ continue;
262
+ if (spec.answerAsk) {
263
+ const questions = prompt.questions?.length ? prompt.questions : [{ id: 'answer' }];
264
+ const record = { kind: 'ask_user', prompt: prompt.prompt ?? '' };
265
+ autoAnswered.push(record);
266
+ writeAnswerBeforeTool(streamOrder, record, stdout, request.streamJson);
267
+ await client.answerAsk(prompt.requestId, { answers: autoAnswer(questions) });
268
+ }
269
+ else {
82
270
  await client.answerAsk(prompt.requestId, {});
271
+ }
83
272
  }
84
273
  }
85
274
  })();
@@ -90,17 +279,86 @@ function watchApprovals(client, policy, onDenied) {
90
279
  },
91
280
  };
92
281
  }
93
- function observeEvent(event, request, stdout, stderr, text) {
282
+ function formatRunDuration(ms) {
283
+ if (!Number.isFinite(ms) || ms < 0)
284
+ return '';
285
+ if (ms < 10_000)
286
+ return `${(Math.round(ms / 100) / 10).toFixed(1)}s`;
287
+ return `${Math.round(ms / 1000)}s`;
288
+ }
289
+ function progressArgs(value) {
290
+ if (!value || typeof value !== 'object')
291
+ return '';
292
+ const record = value;
293
+ for (const key of ['command', 'path', 'file', 'url', 'query']) {
294
+ const field = record[key];
295
+ if (typeof field === 'string' && field.length > 0)
296
+ return field;
297
+ }
298
+ return '';
299
+ }
300
+ function egressTarget(value) {
301
+ if (!value || typeof value !== 'object')
302
+ return '';
303
+ const record = value;
304
+ const host = typeof record.host === 'string' ? record.host : '';
305
+ if (!host)
306
+ return '';
307
+ const port = typeof record.port === 'number' ? record.port : null;
308
+ if (port === null || port === 80 || port === 443)
309
+ return host;
310
+ return `${host}:${port}`;
311
+ }
312
+ function egressProgressLine(args) {
313
+ const target = egressTarget(args);
314
+ return `▸ network_egress ${target} ✓ ${cliText('tui.toolAuto')}`.trimEnd();
315
+ }
316
+ function toolProgressLine(action) {
317
+ const name = typeof action.tool === 'string' && action.tool ? action.tool : 'tool';
318
+ const title = typeof action.view?.title === 'string' ? action.view.title : '';
319
+ const args = title && title !== name ? title : progressArgs(action.arguments);
320
+ const failed = action.success === false || action.error != null;
321
+ const duration = typeof action.durationMs === 'number' ? formatRunDuration(action.durationMs) : '';
322
+ const mark = failed ? '✗' : '✓';
323
+ const base = duration ? `${mark} ${duration}` : mark;
324
+ const cached = action.approval?.kind === 'allow_for_session' && action.approval.cached === true;
325
+ const status = cached ? `${base} 本会话已允许` : base;
326
+ return `▸ ${name} ${args} ${status}`.trimEnd();
327
+ }
328
+ function writeToolProgress(event, stderr, printed) {
329
+ const actions = event.actions;
330
+ if (!Array.isArray(actions))
331
+ return;
332
+ for (const action of actions) {
333
+ if (!action || typeof action !== 'object')
334
+ continue;
335
+ const row = action;
336
+ const settled = typeof row.durationMs === 'number' || typeof row.success === 'boolean' || row.error != null;
337
+ if (!settled)
338
+ continue;
339
+ const name = typeof row.tool === 'string' ? row.tool : 'tool';
340
+ const key = typeof row.id === 'string' && row.id ? row.id : `${name}:${toolProgressLine(row)}`;
341
+ if (printed.has(key))
342
+ continue;
343
+ printed.add(key);
344
+ stderr.write(`${toolProgressLine(row)}\n`);
345
+ }
346
+ }
347
+ function observeEvent(event, request, stdout, stderr, text, streamOrder, printedTools) {
94
348
  if (request.streamJson)
95
- stdout.write(`${JSON.stringify(event)}\n`);
349
+ writeStreamJsonEvent(streamOrder, event, stdout);
96
350
  if (event.type === 'content' && typeof event.content === 'string') {
97
351
  text.push(event.content);
98
352
  if (!request.json && !request.streamJson)
99
353
  stdout.write(event.content);
100
354
  }
101
- if (event.type === 'tool_call' && !request.json && !request.streamJson) {
102
- const name = typeof event.payload?.name === 'string' ? event.payload.name : 'tool';
103
- stderr.write(`tool ${name}\n`);
355
+ if (!request.json && !request.streamJson) {
356
+ if (String(event.type) === 'executed_actions')
357
+ writeToolProgress(event, stderr, printedTools);
358
+ if (event.type === 'tool_call') {
359
+ const name = typeof event.payload?.name === 'string' ? event.payload.name : 'tool';
360
+ stderr.write(`tool ${name}\n`);
361
+ }
104
362
  }
105
363
  if (event.type === 'error') {
106
364
  const message = typeof event.message === 'string' ? event.message : 'error';
@@ -15,6 +15,7 @@ const COMMANDS = [
15
15
  { name: 'tasks', hint: '后台任务' },
16
16
  { name: 'fork', hint: '分叉' },
17
17
  { name: 'rewind', hint: '回退' },
18
+ { name: 'rules', hint: '项目规则' },
18
19
  { name: 'permissions', hint: '会话权限' },
19
20
  { name: 'copy', hint: '复制回答' },
20
21
  { name: 'editor', hint: '外部编辑器' },
package/dist/tui/run.d.ts CHANGED
@@ -1,10 +1,14 @@
1
1
  import { type Terminal } from '@earendil-works/pi-tui';
2
2
  import type { CliRenderer } from '@opentui/core';
3
3
  import type { AgentClient } from '@steerable/agent-client';
4
+ import type { ApprovalSpec } from '../approve.js';
4
5
  export interface RunTuiOptions {
5
6
  client: AgentClient;
6
7
  product: string;
7
8
  dataDir?: string;
9
+ projectId?: string;
10
+ projectLabel?: string;
11
+ approvalPolicy?: ApprovalSpec;
8
12
  terminal?: Terminal;
9
13
  fallbackTerminal?: Terminal;
10
14
  openRenderer?: CliRenderer;
package/dist/tui/run.js CHANGED
@@ -36,15 +36,27 @@ async function runOpenTui(options) {
36
36
  const session = new AgentTui(options.client, {
37
37
  product: options.product,
38
38
  ...(options.dataDir ? { dataDir: options.dataDir } : {}),
39
+ ...(options.projectId ? { projectId: options.projectId } : {}),
40
+ ...(options.projectLabel ? { projectLabel: options.projectLabel } : {}),
41
+ ...(options.approvalPolicy ? { approvalPolicy: options.approvalPolicy } : {}),
39
42
  onChange() {
40
43
  view.apply(session.snapshot());
41
44
  },
42
45
  onExit() {
43
- renderer.destroy();
46
+ stop();
44
47
  resolve(0);
45
48
  },
46
49
  });
47
50
  const view = new OpenTuiView(renderer);
51
+ const onPaste = (event) => {
52
+ event.stopPropagation();
53
+ session.acceptDecodedPaste(new TextDecoder().decode(event.bytes));
54
+ };
55
+ renderer.keyInput.on('paste', onPaste);
56
+ const stop = () => {
57
+ renderer.keyInput.off('paste', onPaste);
58
+ renderer.destroy();
59
+ };
48
60
  renderer.prependInputHandler((sequence) => {
49
61
  if (yieldsToRenderer(sequence))
50
62
  return false;
@@ -57,7 +69,7 @@ async function runOpenTui(options) {
57
69
  return true;
58
70
  });
59
71
  void session.open().then(() => view.apply(session.snapshot()), (error) => {
60
- renderer.destroy();
72
+ stop();
61
73
  reject(error);
62
74
  });
63
75
  });
@@ -69,6 +81,9 @@ function runPlainTui(options) {
69
81
  const session = new AgentTui(options.client, {
70
82
  product: options.product,
71
83
  ...(options.dataDir ? { dataDir: options.dataDir } : {}),
84
+ ...(options.projectId ? { projectId: options.projectId } : {}),
85
+ ...(options.projectLabel ? { projectLabel: options.projectLabel } : {}),
86
+ ...(options.approvalPolicy ? { approvalPolicy: options.approvalPolicy } : {}),
72
87
  onChange() {
73
88
  tui.requestRender();
74
89
  },
@@ -5,6 +5,8 @@ export interface TranscriptLine {
5
5
  name?: string;
6
6
  args?: string;
7
7
  status?: string;
8
+ /** This row was answered by the unattended policy, so the status keeps the auto mark. */
9
+ auto?: boolean;
8
10
  id?: string;
9
11
  output?: string;
10
12
  diff?: string;
@@ -16,6 +18,7 @@ export interface ChatRow {
16
18
  title: string;
17
19
  busy: boolean;
18
20
  selected: boolean;
21
+ projectId?: string | null;
19
22
  }
20
23
  export interface PickRow {
21
24
  label: string;
@@ -39,6 +42,8 @@ export interface TuiScreen {
39
42
  draft: string;
40
43
  cursor: number;
41
44
  status: string;
45
+ projectLabel?: string;
46
+ ruleLabel?: string;
42
47
  picks?: PickRow[] | null;
43
48
  attachments?: string[];
44
49
  children?: ChildRow[];
@@ -49,6 +54,7 @@ export interface TuiScreen {
49
54
  panel?: string[] | null;
50
55
  mode?: 'agent' | 'plan';
51
56
  permission?: 'ask' | 'auto' | 'read';
57
+ approvalPolicy?: 'ask' | 'deny' | 'allow-read' | 'allow-all';
52
58
  todos?: Array<{
53
59
  content: string;
54
60
  status: 'pending' | 'in_progress' | 'completed';
@@ -83,4 +89,6 @@ export declare function draftLine(draft: string, cursor: number): string;
83
89
  export declare function composerRows(draft: string, columns: number): number;
84
90
  export declare const readOnlyText = "\u53EA\u8BFB \u00B7 \u53E6\u4E00\u4E2A\u8FDB\u7A0B\u6B63\u5728\u8FD0\u884C";
85
91
  export declare function visibleText(lines: string[]): string;
92
+ /** 状态行最左侧是无人值守提示,后面用竖线和项目、规则接在同一行。 */
93
+ export declare function projectStatusLine(screen: TuiScreen): string;
86
94
  export declare function bannerLines(screen: TuiScreen): string[];