@aion0/forge 0.10.18 → 0.10.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/RELEASE_NOTES.md +4 -4
- package/app/api/agents/route.ts +4 -2
- package/components/HelpTerminal.tsx +23 -6
- package/components/SettingsModal.tsx +3 -11
- package/lib/agents/index.ts +5 -4
- package/lib/agents/migrate.ts +8 -0
- package/lib/chat/tool-dispatcher.ts +43 -0
- package/lib/help-content.ts +51 -0
- package/lib/help-docs/01-settings.md +0 -1
- package/lib/settings.ts +1 -1
- package/lib/telegram-bot.ts +5 -5
- package/package.json +1 -1
package/RELEASE_NOTES.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Forge v0.10.
|
|
1
|
+
# Forge v0.10.20
|
|
2
2
|
|
|
3
|
-
Released: 2026-05-
|
|
3
|
+
Released: 2026-05-31
|
|
4
4
|
|
|
5
|
-
## Changes since v0.10.
|
|
5
|
+
## Changes since v0.10.19
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.
|
|
8
|
+
**Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.19...v0.10.20
|
package/app/api/agents/route.ts
CHANGED
|
@@ -6,9 +6,11 @@ export async function GET(req: Request) {
|
|
|
6
6
|
const url = new URL(req.url);
|
|
7
7
|
const resolve = url.searchParams.get('resolve');
|
|
8
8
|
|
|
9
|
-
// GET /api/agents?resolve=claude → resolve
|
|
9
|
+
// GET /api/agents?resolve=claude[&scene=help] → resolve launch info for an
|
|
10
|
+
// agent in a given scene (terminal default; help/task/etc. pick models[scene]).
|
|
10
11
|
if (resolve) {
|
|
11
|
-
const
|
|
12
|
+
const scene = url.searchParams.get('scene') as 'terminal' | 'task' | 'telegram' | 'help' | 'mobile' | null;
|
|
13
|
+
const info = resolveTerminalLaunch(resolve, scene || undefined);
|
|
12
14
|
return NextResponse.json(info);
|
|
13
15
|
}
|
|
14
16
|
|
|
@@ -27,7 +27,10 @@ export default function HelpTerminal() {
|
|
|
27
27
|
|
|
28
28
|
let disposed = false;
|
|
29
29
|
let dataDir = '~/.forge/data';
|
|
30
|
-
|
|
30
|
+
// Full launch command for the Help AI: resolved from the DEFAULT agent's
|
|
31
|
+
// `help` scene (binary path + --model models.help + env exports). Falls back
|
|
32
|
+
// to bare `claude` if resolution fails.
|
|
33
|
+
let launchCmd = 'claude';
|
|
31
34
|
|
|
32
35
|
const cs = getComputedStyle(document.documentElement);
|
|
33
36
|
const tv = (name: string) => cs.getPropertyValue(name).trim();
|
|
@@ -76,7 +79,7 @@ export default function HelpTerminal() {
|
|
|
76
79
|
isNewSession = false;
|
|
77
80
|
setTimeout(() => {
|
|
78
81
|
if (socket.readyState === WebSocket.OPEN) {
|
|
79
|
-
socket.send(JSON.stringify({ type: 'input', data: `cd "${dataDir}" 2>/dev/null && ${
|
|
82
|
+
socket.send(JSON.stringify({ type: 'input', data: `cd "${dataDir}" 2>/dev/null && ${launchCmd}\n` }));
|
|
80
83
|
}
|
|
81
84
|
}, 300);
|
|
82
85
|
}
|
|
@@ -97,13 +100,27 @@ export default function HelpTerminal() {
|
|
|
97
100
|
socket.onerror = () => {};
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
// Fetch data dir + default agent then
|
|
103
|
+
// Fetch data dir + default agent, then resolve that agent's `help`-scene
|
|
104
|
+
// launch info (path + model + env) so Help runs the configured binary/model.
|
|
101
105
|
Promise.all([
|
|
102
106
|
fetch('/api/help?action=status').then(r => r.json()).then(data => { if (data.dataDir) dataDir = data.dataDir; }).catch(() => {}),
|
|
103
|
-
fetch('/api/agents').then(r => r.json()).then(data => {
|
|
107
|
+
fetch('/api/agents').then(r => r.json()).then(async (data) => {
|
|
104
108
|
const defaultId = data.defaultAgent || 'claude';
|
|
105
|
-
|
|
106
|
-
|
|
109
|
+
try {
|
|
110
|
+
const info = await fetch(`/api/agents?resolve=${encodeURIComponent(defaultId)}&scene=help`).then(r => r.json());
|
|
111
|
+
const bin = info?.cliCmd || 'claude';
|
|
112
|
+
const modelFlag = info?.model ? ` --model ${info.model}` : '';
|
|
113
|
+
const envPrefix = info?.env
|
|
114
|
+
? Object.entries(info.env as Record<string, string>)
|
|
115
|
+
.map(([k, v]) => `export ${k}=${JSON.stringify(v)}`)
|
|
116
|
+
.join(' && ') + ' && '
|
|
117
|
+
: '';
|
|
118
|
+
launchCmd = `${envPrefix}${bin}${modelFlag}`;
|
|
119
|
+
} catch {
|
|
120
|
+
// Fallback to bare path from the agent list if resolve fails.
|
|
121
|
+
const agent = (data.agents || []).find((a: any) => a.id === defaultId);
|
|
122
|
+
if (agent?.path) launchCmd = agent.path;
|
|
123
|
+
}
|
|
107
124
|
}).catch(() => {}),
|
|
108
125
|
]).finally(() => { if (!disposed) connect(); });
|
|
109
126
|
|
|
@@ -901,7 +901,6 @@ interface AgentEntry {
|
|
|
901
901
|
enabled: boolean;
|
|
902
902
|
type: string;
|
|
903
903
|
taskFlags: string;
|
|
904
|
-
interactiveCmd: string;
|
|
905
904
|
resumeFlag: string;
|
|
906
905
|
outputFormat: string;
|
|
907
906
|
models: { terminal: string; task: string; telegram: string; help: string; mobile: string };
|
|
@@ -1334,7 +1333,7 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1334
1333
|
'generic': { taskFlags: '', resumeFlag: '', outputFormat: 'text', skipPermissionsFlag: '' },
|
|
1335
1334
|
};
|
|
1336
1335
|
const makeNewAgent = (cliType = 'claude-code') => ({
|
|
1337
|
-
id: '', name: '', path: '',
|
|
1336
|
+
id: '', name: '', path: '',
|
|
1338
1337
|
models: { terminal: 'default', task: 'default', telegram: 'default', help: 'default', mobile: 'default' },
|
|
1339
1338
|
requiresTTY: false, cliType,
|
|
1340
1339
|
...cliDefaults[cliType],
|
|
@@ -1369,7 +1368,6 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1369
1368
|
enabled: cfg.enabled !== false,
|
|
1370
1369
|
type: a.type || 'generic',
|
|
1371
1370
|
taskFlags: cfg.taskFlags ?? (a.id === 'claude' ? '-p --verbose --output-format stream-json --dangerously-skip-permissions' : cfg.flags?.join(' ') ?? ''),
|
|
1372
|
-
interactiveCmd: cfg.interactiveCmd ?? a.path,
|
|
1373
1371
|
resumeFlag: cfg.resumeFlag ?? (a.capabilities?.supportsResume ? '-c' : ''),
|
|
1374
1372
|
outputFormat: cfg.outputFormat ?? (a.capabilities?.supportsStreamJson ? 'stream-json' : 'text'),
|
|
1375
1373
|
models: cfg.models ?? { terminal: "default", task: "default", telegram: "default", help: "default", mobile: "default" },
|
|
@@ -1395,7 +1393,6 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1395
1393
|
enabled: cfg.enabled !== false,
|
|
1396
1394
|
type: 'generic',
|
|
1397
1395
|
taskFlags: cfg.taskFlags ?? cfg.flags?.join(' ') ?? '',
|
|
1398
|
-
interactiveCmd: cfg.interactiveCmd ?? cfg.path ?? '',
|
|
1399
1396
|
resumeFlag: cfg.resumeFlag ?? '',
|
|
1400
1397
|
outputFormat: cfg.outputFormat ?? 'text',
|
|
1401
1398
|
models: cfg.models ?? { terminal: "default", task: "default", telegram: "default", help: "default", mobile: "default" },
|
|
@@ -1434,7 +1431,6 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1434
1431
|
path: a.path,
|
|
1435
1432
|
enabled: a.enabled,
|
|
1436
1433
|
taskFlags: a.taskFlags,
|
|
1437
|
-
interactiveCmd: a.interactiveCmd,
|
|
1438
1434
|
resumeFlag: a.resumeFlag,
|
|
1439
1435
|
outputFormat: a.outputFormat,
|
|
1440
1436
|
models: a.models,
|
|
@@ -1527,7 +1523,7 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1527
1523
|
onClick={() => {
|
|
1528
1524
|
// Auto-fill path from detected claude agent when opening Add form
|
|
1529
1525
|
const claude = agents.find(a => a.id === 'claude');
|
|
1530
|
-
if (claude?.path && !newAgent.path) setNewAgent((prev: any) => ({ ...prev, path: claude.path
|
|
1526
|
+
if (claude?.path && !newAgent.path) setNewAgent((prev: any) => ({ ...prev, path: claude.path }));
|
|
1531
1527
|
setShowAdd(v => !v);
|
|
1532
1528
|
}}
|
|
1533
1529
|
className="text-[9px] px-2 py-0.5 border border-[var(--border)] text-[var(--text-secondary)] rounded hover:text-[var(--text-primary)]"
|
|
@@ -1614,10 +1610,6 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1614
1610
|
<label className="text-[9px] text-[var(--text-secondary)]">Task Flags <span className="text-[8px]">(non-interactive mode, e.g. -p --output-format json)</span></label>
|
|
1615
1611
|
<input value={a.taskFlags} onChange={e => updateAgent(a.id, 'taskFlags', e.target.value)} placeholder="-p --verbose" className={inputClass} />
|
|
1616
1612
|
</div>
|
|
1617
|
-
<div>
|
|
1618
|
-
<label className="text-[9px] text-[var(--text-secondary)]">Interactive Command <span className="text-[8px]">(terminal startup)</span></label>
|
|
1619
|
-
<input value={a.interactiveCmd} onChange={e => updateAgent(a.id, 'interactiveCmd', e.target.value)} placeholder="claude" className={inputClass} />
|
|
1620
|
-
</div>
|
|
1621
1613
|
<div className="flex gap-3">
|
|
1622
1614
|
<div className="flex-1">
|
|
1623
1615
|
<label className="text-[9px] text-[var(--text-secondary)]">Resume Flag <span className="text-[8px]">(empty = no resume)</span></label>
|
|
@@ -1770,7 +1762,7 @@ function AgentsSection({ settings, setSettings }: { settings: any; setSettings:
|
|
|
1770
1762
|
// Auto-fill path from detected agent if available
|
|
1771
1763
|
const baseId = ct === 'claude-code' ? 'claude' : ct;
|
|
1772
1764
|
const detected = agents.find(a => a.id === baseId);
|
|
1773
|
-
setNewAgent({ ...newAgent, cliType: ct, ...(cliDefaults[ct] || {}), path: detected?.path || newAgent.path
|
|
1765
|
+
setNewAgent({ ...newAgent, cliType: ct, ...(cliDefaults[ct] || {}), path: detected?.path || newAgent.path });
|
|
1774
1766
|
}} className={inputClass}>
|
|
1775
1767
|
<option value="claude-code">Claude Code</option>
|
|
1776
1768
|
<option value="codex">Codex</option>
|
package/lib/agents/index.ts
CHANGED
|
@@ -240,7 +240,7 @@ export interface TerminalLaunchInfo {
|
|
|
240
240
|
model?: string; // profile model override (--model flag)
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
-
export function resolveTerminalLaunch(agentId?: string): TerminalLaunchInfo {
|
|
243
|
+
export function resolveTerminalLaunch(agentId?: string, scene: 'terminal' | 'task' | 'telegram' | 'help' | 'mobile' = 'terminal'): TerminalLaunchInfo {
|
|
244
244
|
const settings = loadSettings();
|
|
245
245
|
const agentCfg = settings.agents?.[agentId || 'claude'] || {};
|
|
246
246
|
// Resolve cliType: own cliType → base agent's cliType → base agent name guessing → agentId name guessing
|
|
@@ -263,18 +263,19 @@ export function resolveTerminalLaunch(agentId?: string): TerminalLaunchInfo {
|
|
|
263
263
|
|| undefined;
|
|
264
264
|
|
|
265
265
|
// Resolve env/model from this agent's per-use models sub-table.
|
|
266
|
-
// 顶层 model 字段已在 0.10 起被 migrate.ts
|
|
266
|
+
// 顶层 model 字段已在 0.10 起被 migrate.ts 清理,这里按 scene 读 models[scene]
|
|
267
|
+
// (默认 terminal;Help 传 'help' 等)。models[scene] === 'default' → 不加 --model。
|
|
267
268
|
let env: Record<string, string> | undefined;
|
|
268
269
|
let model: string | undefined;
|
|
269
270
|
if (agentCfg.base || agentCfg.env || agentCfg.models) {
|
|
270
271
|
if (agentCfg.env) env = { ...agentCfg.env };
|
|
271
|
-
model = agentCfg.models?.
|
|
272
|
+
model = agentCfg.models?.[scene];
|
|
272
273
|
if (model === 'default') model = undefined; // 'default' = 不加 --model
|
|
273
274
|
} else if (agentCfg.profile) {
|
|
274
275
|
const profileCfg = settings.agents?.[agentCfg.profile];
|
|
275
276
|
if (profileCfg) {
|
|
276
277
|
if (profileCfg.env) env = { ...profileCfg.env };
|
|
277
|
-
model = profileCfg.models?.
|
|
278
|
+
model = profileCfg.models?.[scene];
|
|
278
279
|
if (model === 'default') model = undefined;
|
|
279
280
|
}
|
|
280
281
|
}
|
package/lib/agents/migrate.ts
CHANGED
|
@@ -89,6 +89,14 @@ export function migrateAgentsFlatten(settings: Settings): boolean {
|
|
|
89
89
|
delete raw.model;
|
|
90
90
|
mutated = true;
|
|
91
91
|
}
|
|
92
|
+
|
|
93
|
+
// ── interactiveCmd 字段清理 ──────────────────────────
|
|
94
|
+
// 终端启动统一走 resolveTerminalLaunch(cliCmd = agentCfg.path),
|
|
95
|
+
// interactiveCmd 从未被后端读取,已从 UI / 类型移除。清掉残留死数据。
|
|
96
|
+
if (raw.interactiveCmd !== undefined) {
|
|
97
|
+
delete raw.interactiveCmd;
|
|
98
|
+
mutated = true;
|
|
99
|
+
}
|
|
92
100
|
}
|
|
93
101
|
|
|
94
102
|
// === 兜底校验 defaultAgent / chatAgent ===
|
|
@@ -274,6 +274,30 @@ const BUILTINS: Record<string, BuiltinHandler> = {
|
|
|
274
274
|
});
|
|
275
275
|
return `Task dispatched: ${task.id} (project: ${project.name}, status: ${task.status}). Watch in the Tasks view.`;
|
|
276
276
|
},
|
|
277
|
+
|
|
278
|
+
// List Forge's own help/documentation files so the chat agent can answer
|
|
279
|
+
// questions about Forge itself (features, config, troubleshooting) without
|
|
280
|
+
// the user opening the separate Help AI. Pair with read_help_doc.
|
|
281
|
+
list_help_docs: async () => {
|
|
282
|
+
const { listHelpDocs } = await import('../help-content');
|
|
283
|
+
const docs = listHelpDocs();
|
|
284
|
+
if (docs.length === 0) return 'No Forge help docs found.';
|
|
285
|
+
return 'Forge help docs (read one with read_help_doc):\n' + docs.map((d) => `- ${d}`).join('\n');
|
|
286
|
+
},
|
|
287
|
+
|
|
288
|
+
// Read one Forge help doc in full. Use to answer questions about Forge's
|
|
289
|
+
// own features / settings / troubleshooting.
|
|
290
|
+
read_help_doc: async (input) => {
|
|
291
|
+
const { doc } = (input as { doc?: string } | undefined) || {};
|
|
292
|
+
if (!doc) return 'read_help_doc failed: doc is required (a filename from list_help_docs, e.g. "05-pipelines.md").';
|
|
293
|
+
const { readHelpDoc } = await import('../help-content');
|
|
294
|
+
const content = readHelpDoc(doc);
|
|
295
|
+
if (content == null) return `Help doc "${doc}" not found. Call list_help_docs to see available filenames.`;
|
|
296
|
+
const MAX = 30000;
|
|
297
|
+
return content.length > MAX
|
|
298
|
+
? content.slice(0, MAX) + `\n\n…[truncated — doc is ${content.length} chars]`
|
|
299
|
+
: content;
|
|
300
|
+
},
|
|
277
301
|
};
|
|
278
302
|
|
|
279
303
|
export interface BuiltinToolDef {
|
|
@@ -351,6 +375,25 @@ export const BUILTIN_TOOL_DEFS: BuiltinToolDef[] = [
|
|
|
351
375
|
required: ['prompt'],
|
|
352
376
|
},
|
|
353
377
|
},
|
|
378
|
+
{
|
|
379
|
+
name: 'list_help_docs',
|
|
380
|
+
description: "List Forge's own documentation files. Call this FIRST whenever the user asks how Forge itself works — its features, settings/config, setup, or troubleshooting (e.g. pipelines, schedules, connectors, telegram, tunnel, workspace/smiths, skills, crafts, usage/cost, agents/models). Returns doc filenames; then read the relevant one(s) with read_help_doc and answer from their content. No arguments.",
|
|
381
|
+
input_schema: { type: 'object', properties: {} },
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: 'read_help_doc',
|
|
385
|
+
description: 'Read one Forge help doc in full (filename from list_help_docs, e.g. "05-pipelines.md"). Use its content to answer the user\'s question about Forge. Read more than one doc if the question spans topics.',
|
|
386
|
+
input_schema: {
|
|
387
|
+
type: 'object',
|
|
388
|
+
properties: {
|
|
389
|
+
doc: {
|
|
390
|
+
type: 'string',
|
|
391
|
+
description: 'Help doc filename, e.g. "05-pipelines.md" or "10-troubleshooting.md". Get exact names from list_help_docs.',
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
required: ['doc'],
|
|
395
|
+
},
|
|
396
|
+
},
|
|
354
397
|
];
|
|
355
398
|
|
|
356
399
|
// ─── Connector dispatch ──────────────────────────────────
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help-doc reader for the chat agent's read_help_doc / list_help_docs tools.
|
|
3
|
+
*
|
|
4
|
+
* Reads the markdown docs that ship under lib/help-docs/ (same set the Help AI
|
|
5
|
+
* terminal uses). Resolved relative to this module so it works regardless of
|
|
6
|
+
* whether the on-disk sync (ensureHelpDocs → <config>/help) has run yet, then
|
|
7
|
+
* falls back to the synced copy if the source tree isn't present (installed
|
|
8
|
+
* tarball layouts).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
12
|
+
import { join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { getConfigDir } from './dirs';
|
|
15
|
+
|
|
16
|
+
// Candidate dirs, in priority order: source tree next to this module, then the
|
|
17
|
+
// synced runtime copy under ~/.forge/help.
|
|
18
|
+
function helpDirs(): string[] {
|
|
19
|
+
const dirs: string[] = [];
|
|
20
|
+
try {
|
|
21
|
+
const here = fileURLToPath(new URL('./help-docs', import.meta.url));
|
|
22
|
+
dirs.push(here);
|
|
23
|
+
} catch { /* import.meta.url unavailable (CJS bundle) — skip */ }
|
|
24
|
+
dirs.push(join(getConfigDir(), 'help'));
|
|
25
|
+
return dirs;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** List available help-doc filenames (e.g. ["00-overview.md", ...]), sorted. */
|
|
29
|
+
export function listHelpDocs(): string[] {
|
|
30
|
+
for (const dir of helpDirs()) {
|
|
31
|
+
try {
|
|
32
|
+
if (!existsSync(dir)) continue;
|
|
33
|
+
const files = readdirSync(dir).filter((f) => f.endsWith('.md')).sort();
|
|
34
|
+
if (files.length) return files;
|
|
35
|
+
} catch { /* try next dir */ }
|
|
36
|
+
}
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Read one help doc by filename. Returns null if not found. Path-traversal safe. */
|
|
41
|
+
export function readHelpDoc(name: string): string | null {
|
|
42
|
+
const base = (name || '').split(/[\\/]/).pop() || '';
|
|
43
|
+
if (!base.endsWith('.md')) return null;
|
|
44
|
+
for (const dir of helpDirs()) {
|
|
45
|
+
try {
|
|
46
|
+
const file = join(dir, base);
|
|
47
|
+
if (existsSync(file)) return readFileSync(file, 'utf-8');
|
|
48
|
+
} catch { /* try next dir */ }
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
@@ -46,7 +46,6 @@ Each agent entry in `settings.agents` supports:
|
|
|
46
46
|
| `enabled` | boolean | Whether this agent is available |
|
|
47
47
|
| `cliType` | string | CLI tool type: `claude-code`, `codex`, `aider`, `generic` |
|
|
48
48
|
| `taskFlags` | string | Flags for headless task execution (e.g. `-p --verbose`) |
|
|
49
|
-
| `interactiveCmd` | string | Command for interactive terminal sessions |
|
|
50
49
|
| `resumeFlag` | string | Flag to resume sessions (e.g. `-c` for claude) |
|
|
51
50
|
| `outputFormat` | string | Output format: `stream-json`, `text` |
|
|
52
51
|
| `skipPermissionsFlag` | string | Flag to skip permissions (e.g. `--dangerously-skip-permissions`) |
|
package/lib/settings.ts
CHANGED
|
@@ -23,7 +23,7 @@ const SETTINGS_FILE = join(DATA_DIR, 'settings.yaml');
|
|
|
23
23
|
export interface AgentEntry {
|
|
24
24
|
tool?: 'claude' | 'codex' | 'aider' | 'opencode'; // which CLI binary
|
|
25
25
|
path?: string; name?: string; enabled?: boolean;
|
|
26
|
-
flags?: string[]; taskFlags?: string;
|
|
26
|
+
flags?: string[]; taskFlags?: string; resumeFlag?: string; outputFormat?: string;
|
|
27
27
|
models?: { terminal?: string; task?: string; telegram?: string; help?: string; mobile?: string };
|
|
28
28
|
skipPermissionsFlag?: string;
|
|
29
29
|
requiresTTY?: boolean;
|
package/lib/telegram-bot.ts
CHANGED
|
@@ -1086,7 +1086,7 @@ function getSessionPreview(sessionName: string, maxLines: number = 1): string {
|
|
|
1086
1086
|
try {
|
|
1087
1087
|
const { execSync } = require('node:child_process');
|
|
1088
1088
|
// Capture last screen, strip ANSI escapes, find last non-empty lines
|
|
1089
|
-
const out = execSync(`tmux capture-pane -t "${sessionName}" -p -S -
|
|
1089
|
+
const out = execSync(`tmux capture-pane -t "${sessionName}" -p -S -100 2>/dev/null`, {
|
|
1090
1090
|
encoding: 'utf-8',
|
|
1091
1091
|
timeout: 2000,
|
|
1092
1092
|
}) as string;
|
|
@@ -1101,8 +1101,8 @@ function getSessionPreview(sessionName: string, maxLines: number = 1): string {
|
|
|
1101
1101
|
const last = tail[0];
|
|
1102
1102
|
return last.length > 30 ? last.slice(0, 30) + '…' : last;
|
|
1103
1103
|
}
|
|
1104
|
-
// Multi-line: truncate each line to
|
|
1105
|
-
return tail.map(l => l.length >
|
|
1104
|
+
// Multi-line: truncate each line to 160 chars
|
|
1105
|
+
return tail.map(l => l.length > 160 ? l.slice(0, 160) + '…' : l).join('\n');
|
|
1106
1106
|
} catch {
|
|
1107
1107
|
return '';
|
|
1108
1108
|
}
|
|
@@ -1185,8 +1185,8 @@ async function pickInjectTarget(chatId: number, numStr: string) {
|
|
|
1185
1185
|
scheduleInjectAutoClear(chatId);
|
|
1186
1186
|
const labelMap = getSessionLabels();
|
|
1187
1187
|
const display = labelMap[sessionName] || sessionName.replace(/^mw-?/, '');
|
|
1188
|
-
// Show last
|
|
1189
|
-
const context = getSessionPreview(sessionName,
|
|
1188
|
+
// Show last 16 lines of context so user knows what's in the terminal
|
|
1189
|
+
const context = getSessionPreview(sessionName, 16);
|
|
1190
1190
|
const contextBlock = context ? `\n\n📺 Last output:\n\`\`\`\n${context}\n\`\`\`` : '';
|
|
1191
1191
|
await send(chatId,
|
|
1192
1192
|
`🎯 Target: ${display}${contextBlock}\n\n` +
|
package/package.json
CHANGED