triflux 10.8.1 → 10.9.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.
- package/hooks/keyword-rules.json +63 -0
- package/hooks/safety-guard.mjs +7 -1
- package/hub/lib/env-detect.mjs +85 -0
- package/hub/team/cli/commands/start/start-wt.mjs +1 -1
- package/hub/team/conductor.mjs +150 -23
- package/hub/team/dashboard-open.mjs +30 -34
- package/hub/team/headless.mjs +67 -206
- package/hub/team/session.mjs +24 -126
- package/hub/team/swarm-hypervisor.mjs +62 -4
- package/hub/team/tui.mjs +10 -36
- package/hub/team/wt-manager.mjs +250 -19
- package/package.json +1 -1
- package/references/hosts.json +2 -2
- package/scripts/__tests__/keyword-detector.test.mjs +3 -3
- package/scripts/keyword-detector.mjs +21 -0
- package/scripts/lib/keyword-rules.mjs +7 -0
package/hooks/keyword-rules.json
CHANGED
|
@@ -190,6 +190,69 @@
|
|
|
190
190
|
"state": null,
|
|
191
191
|
"mcp_route": null
|
|
192
192
|
},
|
|
193
|
+
{
|
|
194
|
+
"id": "wt-tab-route",
|
|
195
|
+
"patterns": [
|
|
196
|
+
{ "source": "(?:새\\s*탭|탭\\s*(?:새로|추가|생성|열|띄|파|만들))", "flags": "i" },
|
|
197
|
+
{ "source": "(?:패인|화면|pane)\\s*(?:분할|나눠|스플릿|split)", "flags": "i" },
|
|
198
|
+
{ "source": "\\bwt\\b\\s*(?:에\\s*)?(?:탭|tab|열|띄|새)", "flags": "i" },
|
|
199
|
+
{ "source": "(?:터미널|terminal)\\s*(?:탭|새\\s*탭)", "flags": "i" },
|
|
200
|
+
{ "source": "\\b(?:new\\s+tab|split\\s+pane|open\\s+(?:new\\s+)?tab)\\b", "flags": "i" }
|
|
201
|
+
],
|
|
202
|
+
"skill": null,
|
|
203
|
+
"action": "context_hint",
|
|
204
|
+
"hint": "WT 탭/패인 생성 요청입니다. wt.exe 직접 호출은 safety-guard가 차단합니다.\n\nhub/team/wt-manager.mjs의 createWtManager() 팩토리로 인스턴스를 만든 뒤 API를 호출하세요:\n- wt.createTab({ title, command, profile, cwd }) — 새 탭\n- wt.splitPane({ direction: 'H'|'V', title, command }) — 패인 분할\n- wt.applySplitLayout([{ title, command, direction }]) — 다중 배치\n- wt.closeTab(title) — 탭 닫기\n\n사용법:\nnode -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.createTab({ title: 'MyTab', command: 'pwsh' }); })\"",
|
|
205
|
+
"priority": 1,
|
|
206
|
+
"supersedes": ["tfx-unified"],
|
|
207
|
+
"exclusive": false,
|
|
208
|
+
"state": null,
|
|
209
|
+
"mcp_route": null
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
"id": "wt-tab-rename",
|
|
213
|
+
"patterns": [
|
|
214
|
+
{ "source": "(?:탭\\s*(?:이름|제목)\\s*(?:바꿔|변경|rename))", "flags": "i" },
|
|
215
|
+
{ "source": "(?:rename\\s+tab|tab\\s+rename)", "flags": "i" }
|
|
216
|
+
],
|
|
217
|
+
"skill": null,
|
|
218
|
+
"action": "context_hint",
|
|
219
|
+
"hint": "WT 탭 이름 변경 요청입니다. wt.exe 직접 호출은 safety-guard가 차단합니다.\n\nhub/team/wt-manager.mjs의 createWtManager() 팩토리로 인스턴스를 만든 뒤 API를 호출하세요:\n- wt.renameTab({ oldTitle, newTitle }) — 탭 이름 변경\n\n사용법:\nnode -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.renameTab({ oldTitle: 'OldName', newTitle: 'NewName' }); })\"",
|
|
220
|
+
"priority": 1,
|
|
221
|
+
"supersedes": ["tfx-unified"],
|
|
222
|
+
"exclusive": false,
|
|
223
|
+
"state": null,
|
|
224
|
+
"mcp_route": null
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"id": "wt-tab-list",
|
|
228
|
+
"patterns": [
|
|
229
|
+
{ "source": "(?:탭\\s*(?:목록|리스트|열린|현재))", "flags": "i" },
|
|
230
|
+
{ "source": "(?:list\\s+tabs|tab\\s+list|열린\\s*탭)", "flags": "i" }
|
|
231
|
+
],
|
|
232
|
+
"skill": null,
|
|
233
|
+
"action": "context_hint",
|
|
234
|
+
"hint": "WT 탭 목록 조회 요청입니다. wt.exe 직접 호출은 safety-guard가 차단합니다.\n\nhub/team/wt-manager.mjs의 createWtManager() 팩토리로 인스턴스를 만든 뒤 API를 호출하세요:\n- wt.listTabs() — 현재 열린 탭 목록 (pid 파일 기반)\n\n사용법:\nnode -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); console.log(wt.listTabs()); })\"",
|
|
235
|
+
"priority": 1,
|
|
236
|
+
"supersedes": ["tfx-unified"],
|
|
237
|
+
"exclusive": false,
|
|
238
|
+
"state": null,
|
|
239
|
+
"mcp_route": null
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
"id": "wt-tab-close",
|
|
243
|
+
"patterns": [
|
|
244
|
+
{ "source": "(?:탭\\s*(?:닫아|닫기|종료|정리|close))", "flags": "i" },
|
|
245
|
+
{ "source": "(?:close\\s+tab|tab\\s+close|탭\\s*정리)", "flags": "i" }
|
|
246
|
+
],
|
|
247
|
+
"skill": null,
|
|
248
|
+
"action": "context_hint",
|
|
249
|
+
"hint": "WT 탭 닫기 요청입니다. wt.exe 직접 호출은 safety-guard가 차단합니다.\n\nhub/team/wt-manager.mjs의 createWtManager() 팩토리로 인스턴스를 만든 뒤 API를 호출하세요:\n- wt.closeTab(title) — 제목으로 탭 닫기\n- wt.closeStale({ olderThanMs, titlePattern }) — 오래된/패턴 매칭 탭 정리\n\n사용법:\nnode -e \"import('./hub/team/wt-manager.mjs').then(m => { const wt = m.createWtManager(); wt.closeTab('MyTab'); })\"",
|
|
250
|
+
"priority": 1,
|
|
251
|
+
"supersedes": ["tfx-unified"],
|
|
252
|
+
"exclusive": false,
|
|
253
|
+
"state": null,
|
|
254
|
+
"mcp_route": null
|
|
255
|
+
},
|
|
193
256
|
{
|
|
194
257
|
"id": "handoff-route",
|
|
195
258
|
"patterns": [
|
package/hooks/safety-guard.mjs
CHANGED
|
@@ -65,7 +65,13 @@ const WT_DIRECT_PATTERNS = [
|
|
|
65
65
|
];
|
|
66
66
|
|
|
67
67
|
const WT_DIRECT_BLOCK_MESSAGE =
|
|
68
|
-
"[safety-guard] wt.exe 직접 호출
|
|
68
|
+
"[safety-guard] wt.exe 직접 호출 차단됨.\n" +
|
|
69
|
+
"→ hub/team/wt-manager.mjs의 createWtManager() 팩토리 사용:\n" +
|
|
70
|
+
" wt.createTab({ title, command, profile, cwd }) — 새 탭\n" +
|
|
71
|
+
" wt.splitPane({ direction: 'H'|'V', title, command }) — 패인 분할\n" +
|
|
72
|
+
" wt.applySplitLayout([{ title, command, direction }]) — 다중 배치\n" +
|
|
73
|
+
'사용법: node -e "import(\'./hub/team/wt-manager.mjs\').then(m => { const wt = m.createWtManager(); wt.createTab({ title: \'제목\', command: \'pwsh\' }); })"';
|
|
74
|
+
|
|
69
75
|
|
|
70
76
|
// ── SSH+PowerShell bash 문법 차단 ────────────────────────────
|
|
71
77
|
// 원격 기본 셸이 PowerShell인 호스트에 bash redirect/glob을 보내면 오동작
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// hub/lib/env-detect.mjs — 쉘/터미널/멀티플렉서 환경 감지
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import { platform as osPlatform } from "node:os";
|
|
4
|
+
|
|
5
|
+
let _cached = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 기본 쉘 감지
|
|
9
|
+
* Windows: pwsh → powershell
|
|
10
|
+
* Unix: $SHELL → /bin/sh
|
|
11
|
+
*/
|
|
12
|
+
export function detectShell() {
|
|
13
|
+
const platform = osPlatform();
|
|
14
|
+
if (platform === "win32") {
|
|
15
|
+
try {
|
|
16
|
+
execFileSync("where", ["pwsh.exe"], { stdio: "ignore", timeout: 3000 });
|
|
17
|
+
return { name: "pwsh", path: "pwsh.exe", version: null };
|
|
18
|
+
} catch {
|
|
19
|
+
try {
|
|
20
|
+
execFileSync("where", ["powershell.exe"], { stdio: "ignore", timeout: 3000 });
|
|
21
|
+
return { name: "powershell", path: "powershell.exe", version: null };
|
|
22
|
+
} catch {
|
|
23
|
+
return { name: "cmd", path: "cmd.exe", version: null, installHint: "pwsh: winget install Microsoft.PowerShell" };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const shellPath = process.env.SHELL || "/bin/sh";
|
|
29
|
+
const name = shellPath.split("/").pop() || "sh";
|
|
30
|
+
return { name, path: shellPath, version: null };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 터미널 에뮬레이터 감지
|
|
35
|
+
*/
|
|
36
|
+
export function detectTerminal() {
|
|
37
|
+
const platform = osPlatform();
|
|
38
|
+
if (platform === "win32") {
|
|
39
|
+
try {
|
|
40
|
+
execFileSync("where", ["wt.exe"], { stdio: "ignore", timeout: 3000 });
|
|
41
|
+
return { name: "windows-terminal", hasWt: true };
|
|
42
|
+
} catch {
|
|
43
|
+
return { name: "conhost", hasWt: false, installHint: "Windows Terminal: winget install Microsoft.WindowsTerminal" };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.env.TERM_PROGRAM === "iTerm.app") {
|
|
48
|
+
return { name: "iterm2", hasWt: false };
|
|
49
|
+
}
|
|
50
|
+
if (process.env.TERM_PROGRAM === "Apple_Terminal") {
|
|
51
|
+
return { name: "terminal-app", hasWt: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { name: "unknown", hasWt: false };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* 멀티플렉서 감지 (tmux)
|
|
59
|
+
*/
|
|
60
|
+
export function detectMultiplexer() {
|
|
61
|
+
try {
|
|
62
|
+
const cmd = osPlatform() === "win32" ? "where" : "which";
|
|
63
|
+
const path = execFileSync(cmd, ["tmux"], { encoding: "utf8", timeout: 3000, stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
64
|
+
return { name: "tmux", path };
|
|
65
|
+
} catch {
|
|
66
|
+
return { name: "none", path: null };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 통합 환경 정보 조회 (레이지 싱글톤 캐시)
|
|
72
|
+
* @returns {{ shell: object, terminal: object, multiplexer: object, platform: string }}
|
|
73
|
+
*/
|
|
74
|
+
export function getEnvironment() {
|
|
75
|
+
if (_cached) return _cached;
|
|
76
|
+
|
|
77
|
+
_cached = Object.freeze({
|
|
78
|
+
shell: detectShell(),
|
|
79
|
+
terminal: detectTerminal(),
|
|
80
|
+
multiplexer: detectMultiplexer(),
|
|
81
|
+
platform: osPlatform(),
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return _cached;
|
|
85
|
+
}
|
|
@@ -19,7 +19,7 @@ export async function startWtTeam({
|
|
|
19
19
|
warn(`wt 모드에서 ${layout} 레이아웃은 미지원 — ${effectiveLayout}로 대체`);
|
|
20
20
|
console.log(` 레이아웃: ${effectiveLayout} (${paneCount} panes)`);
|
|
21
21
|
|
|
22
|
-
const session = createWtSession(sessionId, {
|
|
22
|
+
const session = await createWtSession(sessionId, {
|
|
23
23
|
layout: effectiveLayout,
|
|
24
24
|
paneCommands: [
|
|
25
25
|
{ title: `${sessionId}-lead`, command: buildCliCommand(lead) },
|
package/hub/team/conductor.mjs
CHANGED
|
@@ -28,7 +28,7 @@ import {
|
|
|
28
28
|
} from "./conductor-registry.mjs";
|
|
29
29
|
import { createEventLog } from "./event-log.mjs";
|
|
30
30
|
import { createHealthProbe } from "./health-probe.mjs";
|
|
31
|
-
import { buildLauncher } from "./launcher-template.mjs";
|
|
31
|
+
import { buildLauncher, getAdapter } from "./launcher-template.mjs";
|
|
32
32
|
import { createRemoteProbe } from "./remote-probe.mjs";
|
|
33
33
|
|
|
34
34
|
/** 세션 상태 */
|
|
@@ -335,7 +335,11 @@ export function createConductor(opts = {}) {
|
|
|
335
335
|
const backoffMs = Math.min(1000 * 2 ** (session.restarts - 1), 30_000);
|
|
336
336
|
session._respawnTimer = setTimeout(() => {
|
|
337
337
|
session._respawnTimer = null;
|
|
338
|
-
|
|
338
|
+
if (session.config.remote) {
|
|
339
|
+
startRemoteSession(session);
|
|
340
|
+
} else {
|
|
341
|
+
void respawnSession(session);
|
|
342
|
+
}
|
|
339
343
|
}, backoffMs);
|
|
340
344
|
} else {
|
|
341
345
|
transition(session, STATES.DEAD, `maxRestarts(${maxRestarts})_exceeded`);
|
|
@@ -400,6 +404,10 @@ export function createConductor(opts = {}) {
|
|
|
400
404
|
session.outPath = outPath;
|
|
401
405
|
session.errPath = errPath;
|
|
402
406
|
|
|
407
|
+
// 로컬 세션: 프롬프트는 CLI args로 전달되므로 stdin 즉시 닫기
|
|
408
|
+
// (codex가 stdin pipe 감지 시 "Reading additional input..." 대기 방지)
|
|
409
|
+
if (child.stdin) child.stdin.end();
|
|
410
|
+
|
|
403
411
|
eventLog.append("spawn", {
|
|
404
412
|
session: session.id,
|
|
405
413
|
agent: session.config.agent,
|
|
@@ -510,40 +518,159 @@ export function createConductor(opts = {}) {
|
|
|
510
518
|
}
|
|
511
519
|
|
|
512
520
|
/**
|
|
513
|
-
* 원격 세션 시작 — child process
|
|
514
|
-
* 원격
|
|
521
|
+
* 원격 세션 시작 — SSH child process로 직접 실행.
|
|
522
|
+
* psmux 불필요. 원격 호스트에서 claude -p 를 SSH 경유로 실행하고
|
|
523
|
+
* 로컬 child process로 관리한다.
|
|
515
524
|
*/
|
|
516
525
|
function startRemoteSession(session) {
|
|
517
526
|
transition(session, STATES.STARTING, "remote_initial");
|
|
518
527
|
|
|
519
|
-
const { host,
|
|
520
|
-
|
|
521
|
-
|
|
528
|
+
const { host, prompt, agent, workdir: remoteWorkdir } = session.config;
|
|
529
|
+
|
|
530
|
+
// SSH args를 배열로 구성하여 셸 이스케이프 문제 회피
|
|
531
|
+
const cdPrefix = remoteWorkdir ? `cd ${remoteWorkdir} && ` : "";
|
|
532
|
+
|
|
533
|
+
let remoteBin;
|
|
534
|
+
if (agent === "claude") {
|
|
535
|
+
remoteBin = "claude --output-format text";
|
|
536
|
+
} else if (agent === "gemini") {
|
|
537
|
+
remoteBin = "gemini -y";
|
|
538
|
+
} else {
|
|
539
|
+
remoteBin = "codex exec -s danger-full-access --dangerously-bypass-approvals-and-sandbox";
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// prompt는 stdin으로 전달 — 셸 이스케이프 문제 완전 회피
|
|
543
|
+
const sshArgs = [
|
|
544
|
+
"-o", "ConnectTimeout=30",
|
|
545
|
+
"-o", "BatchMode=yes",
|
|
546
|
+
host,
|
|
547
|
+
`${cdPrefix}${remoteBin}`,
|
|
548
|
+
];
|
|
549
|
+
|
|
550
|
+
const sshCmd = `ssh ${sshArgs.join(" ")}`;
|
|
522
551
|
|
|
523
552
|
eventLog.append("remote_start", {
|
|
524
553
|
session: session.id,
|
|
525
554
|
host,
|
|
526
|
-
|
|
527
|
-
sessionName: resolvedSessionName,
|
|
555
|
+
command: sshCmd,
|
|
528
556
|
});
|
|
529
557
|
|
|
558
|
+
const outPath = join(logsDir, `${session.id}.out.log`);
|
|
559
|
+
const errPath = join(logsDir, `${session.id}.err.log`);
|
|
560
|
+
const outWs = createWriteStream(outPath, { flags: "a" });
|
|
561
|
+
const errWs = createWriteStream(errPath, { flags: "a" });
|
|
562
|
+
|
|
563
|
+
let child;
|
|
564
|
+
try {
|
|
565
|
+
child = spawn("ssh", sshArgs, {
|
|
566
|
+
env: process.env,
|
|
567
|
+
reason: `conductor:remoteSession:${session.id}`,
|
|
568
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
569
|
+
windowsHide: true,
|
|
570
|
+
});
|
|
571
|
+
} catch (err) {
|
|
572
|
+
eventLog.append("spawn_error", {
|
|
573
|
+
session: session.id,
|
|
574
|
+
error: err.message,
|
|
575
|
+
});
|
|
576
|
+
handleFailure(session, `spawn_error:${err.message}`);
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
session.child = child;
|
|
581
|
+
session.outPath = outPath;
|
|
582
|
+
session.errPath = errPath;
|
|
530
583
|
session.alive = true;
|
|
531
584
|
|
|
532
|
-
//
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
585
|
+
// stdin으로 프롬프트 전달 — 셸 이스케이프 완전 우회
|
|
586
|
+
if (child.stdin) {
|
|
587
|
+
child.stdin.write(prompt);
|
|
588
|
+
child.stdin.end();
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
eventLog.append("spawn", {
|
|
592
|
+
session: session.id,
|
|
593
|
+
agent,
|
|
594
|
+
pid: child.pid,
|
|
595
|
+
command: sshCmd,
|
|
596
|
+
remote: true,
|
|
597
|
+
host,
|
|
598
|
+
});
|
|
599
|
+
|
|
600
|
+
// stdout/stderr 추적
|
|
601
|
+
let outputBytes = 0;
|
|
602
|
+
const trackOutput = (buf) => {
|
|
603
|
+
outputBytes += buf.length;
|
|
604
|
+
};
|
|
605
|
+
|
|
606
|
+
child.stdout?.on("data", (buf) => {
|
|
607
|
+
trackOutput(buf);
|
|
608
|
+
outWs.write(buf);
|
|
609
|
+
});
|
|
610
|
+
child.stderr?.on("data", (buf) => {
|
|
611
|
+
trackOutput(buf);
|
|
612
|
+
errWs.write(buf);
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
// 상태를 healthy로 전이
|
|
616
|
+
transition(session, STATES.HEALTHY, "remote_ssh_spawned");
|
|
617
|
+
|
|
618
|
+
// Health probe — 주기적으로 출력 변화 체크 (F3 stall 감지)
|
|
619
|
+
let lastBytes = 0;
|
|
620
|
+
let stallChecks = 0;
|
|
621
|
+
const stallThreshold = probeOpts.stallChecks || 8; // 8 * intervalMs = stall
|
|
622
|
+
const healthInterval = setInterval(() => {
|
|
623
|
+
if (TERMINAL_STATES.has(session.state)) {
|
|
624
|
+
clearInterval(healthInterval);
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (outputBytes > lastBytes) {
|
|
628
|
+
lastBytes = outputBytes;
|
|
629
|
+
stallChecks = 0;
|
|
630
|
+
if (session.state !== STATES.HEALTHY) {
|
|
631
|
+
transition(session, STATES.HEALTHY, "output_advancing");
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
stallChecks++;
|
|
635
|
+
if (stallChecks >= stallThreshold && session.state === STATES.HEALTHY) {
|
|
636
|
+
transition(session, STATES.STALLED, "remote_no_output");
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}, probeOpts.intervalMs || 5000);
|
|
640
|
+
|
|
641
|
+
// exit 핸들러
|
|
642
|
+
child.once("exit", (code) => {
|
|
643
|
+
clearInterval(healthInterval);
|
|
644
|
+
outWs.end();
|
|
645
|
+
errWs.end();
|
|
646
|
+
session.alive = false;
|
|
647
|
+
|
|
648
|
+
eventLog.append("remote_exit", {
|
|
649
|
+
session: session.id,
|
|
650
|
+
code,
|
|
651
|
+
outputBytes,
|
|
536
652
|
host,
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
if (code === 0) {
|
|
656
|
+
transition(session, STATES.COMPLETED, `exit_${code}`);
|
|
657
|
+
emitter.emit("completed", { sessionId: session.id });
|
|
658
|
+
maybeAutoShutdown();
|
|
659
|
+
} else {
|
|
660
|
+
handleFailure(session, `remote_exit_${code}`);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
child.once("error", (err) => {
|
|
665
|
+
clearInterval(healthInterval);
|
|
666
|
+
outWs.end();
|
|
667
|
+
errWs.end();
|
|
668
|
+
eventLog.append("spawn_error", {
|
|
669
|
+
session: session.id,
|
|
670
|
+
error: err.message,
|
|
671
|
+
});
|
|
672
|
+
handleFailure(session, `spawn_error:${err.message}`);
|
|
673
|
+
});
|
|
547
674
|
}
|
|
548
675
|
|
|
549
676
|
/**
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { spawn } from "../lib/spawn-trace.mjs";
|
|
2
|
-
|
|
3
1
|
import { psmuxExec } from "./psmux.mjs";
|
|
4
2
|
import {
|
|
5
3
|
detectMultiplexer,
|
|
@@ -8,6 +6,7 @@ import {
|
|
|
8
6
|
resolveAttachCommand,
|
|
9
7
|
tmuxExec,
|
|
10
8
|
} from "./session.mjs";
|
|
9
|
+
import { createWtManager } from "./wt-manager.mjs";
|
|
11
10
|
|
|
12
11
|
function sanitizeWindowTitle(value, fallback = "triflux") {
|
|
13
12
|
const text = String(value || "")
|
|
@@ -44,9 +43,10 @@ export function decideDashboardOpenMode({
|
|
|
44
43
|
return hasWtSession ? "split" : "window";
|
|
45
44
|
}
|
|
46
45
|
|
|
47
|
-
function spawnWindowsTerminal(spec, opts = {}) {
|
|
46
|
+
async function spawnWindowsTerminal(spec, opts = {}) {
|
|
48
47
|
if (!hasWindowsTerminal()) return false;
|
|
49
48
|
|
|
49
|
+
const wt = createWtManager();
|
|
50
50
|
const {
|
|
51
51
|
mode = "window",
|
|
52
52
|
title = "triflux",
|
|
@@ -56,35 +56,29 @@ function spawnWindowsTerminal(spec, opts = {}) {
|
|
|
56
56
|
|
|
57
57
|
const safeTitle = sanitizeWindowTitle(title);
|
|
58
58
|
const safeCwd = sanitizeWorkingDirectory(cwd);
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
detached: true,
|
|
83
|
-
stdio: "ignore",
|
|
84
|
-
windowsHide: false,
|
|
85
|
-
});
|
|
86
|
-
child.unref();
|
|
87
|
-
return true;
|
|
59
|
+
|
|
60
|
+
try {
|
|
61
|
+
if (mode === "split") {
|
|
62
|
+
await wt.splitPane({
|
|
63
|
+
direction: split?.orientation === "V" ? "V" : "H",
|
|
64
|
+
size: (split?.size || 0.5) * 100,
|
|
65
|
+
title: safeTitle,
|
|
66
|
+
cwd: safeCwd,
|
|
67
|
+
command: spec.args ? `${spec.command} ${spec.args.join(" ")}` : spec.command,
|
|
68
|
+
profile: "triflux",
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
await wt.createTab({
|
|
72
|
+
title: safeTitle,
|
|
73
|
+
cwd: safeCwd,
|
|
74
|
+
command: spec.args ? `${spec.command} ${spec.args.join(" ")}` : spec.command,
|
|
75
|
+
profile: "triflux",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
} catch {
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
88
82
|
}
|
|
89
83
|
|
|
90
84
|
export function focusManagedPane(target, opts = {}) {
|
|
@@ -122,7 +116,7 @@ export function openHeadlessDashboardTarget(sessionName, opts = {}) {
|
|
|
122
116
|
}
|
|
123
117
|
|
|
124
118
|
// 전체 열기 (Shift+Enter) → 새 WT 창으로 세션 attach
|
|
125
|
-
|
|
119
|
+
void spawnWindowsTerminal(
|
|
126
120
|
{ command: "psmux", args: ["attach-session", "-t", safeSession] },
|
|
127
121
|
{
|
|
128
122
|
mode: decideDashboardOpenMode({ openAll }),
|
|
@@ -130,6 +124,7 @@ export function openHeadlessDashboardTarget(sessionName, opts = {}) {
|
|
|
130
124
|
cwd,
|
|
131
125
|
},
|
|
132
126
|
);
|
|
127
|
+
return true;
|
|
133
128
|
}
|
|
134
129
|
|
|
135
130
|
export function openDashboardRuntimeTarget(runtime, opts = {}) {
|
|
@@ -162,11 +157,12 @@ export function openDashboardRuntimeTarget(runtime, opts = {}) {
|
|
|
162
157
|
try {
|
|
163
158
|
if (!openAll && targetPane)
|
|
164
159
|
focusManagedPane(targetPane, { teammateMode, layout });
|
|
165
|
-
|
|
160
|
+
void spawnWindowsTerminal(resolveAttachCommand(sessionName), {
|
|
166
161
|
mode: decideDashboardOpenMode({ openAll }),
|
|
167
162
|
title: title || `▲ ${sanitizeSessionName(sessionName)}`,
|
|
168
163
|
cwd,
|
|
169
164
|
});
|
|
165
|
+
return true;
|
|
170
166
|
} catch {
|
|
171
167
|
return false;
|
|
172
168
|
}
|