@aion0/forge 0.2.8 → 0.2.10
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/app/api/monitor/route.ts +58 -0
- package/check-forge-status.sh +53 -0
- package/components/Dashboard.tsx +10 -0
- package/components/DocTerminal.tsx +11 -3
- package/components/MonitorPanel.tsx +102 -0
- package/components/SettingsModal.tsx +18 -0
- package/components/WebTerminal.tsx +17 -5
- package/lib/settings.ts +2 -0
- package/lib/telegram-bot.ts +3 -3
- package/next-env.d.ts +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NextResponse } from 'next/server';
|
|
2
|
+
import { execSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
function run(cmd: string): string {
|
|
5
|
+
try {
|
|
6
|
+
return execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
7
|
+
} catch { return ''; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function countProcess(pattern: string): { count: number; pid: string } {
|
|
11
|
+
const out = run(`ps aux | grep '${pattern}' | grep -v grep | head -1`);
|
|
12
|
+
const pid = out ? out.split(/\s+/)[1] || '' : '';
|
|
13
|
+
const count = out ? run(`ps aux | grep '${pattern}' | grep -v grep | wc -l`).trim() : '0';
|
|
14
|
+
return { count: parseInt(count), pid };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function GET() {
|
|
18
|
+
// Processes
|
|
19
|
+
const nextjs = countProcess('next-server');
|
|
20
|
+
const terminal = countProcess('terminal-standalone');
|
|
21
|
+
const telegram = countProcess('telegram-standalone');
|
|
22
|
+
const tunnel = countProcess('cloudflared tunnel');
|
|
23
|
+
|
|
24
|
+
// Tunnel URL
|
|
25
|
+
let tunnelUrl = '';
|
|
26
|
+
try {
|
|
27
|
+
const { readFileSync } = require('fs');
|
|
28
|
+
const { join } = require('path');
|
|
29
|
+
const { homedir } = require('os');
|
|
30
|
+
const state = JSON.parse(readFileSync(join(homedir(), '.forge', 'tunnel-state.json'), 'utf-8'));
|
|
31
|
+
tunnelUrl = state.url || '';
|
|
32
|
+
} catch {}
|
|
33
|
+
|
|
34
|
+
// tmux sessions
|
|
35
|
+
let sessions: { name: string; created: string; attached: boolean; windows: number }[] = [];
|
|
36
|
+
try {
|
|
37
|
+
const out = run("tmux list-sessions -F '#{session_name}||#{session_created}||#{session_attached}||#{session_windows}' 2>/dev/null");
|
|
38
|
+
sessions = out.split('\n').filter(l => l.startsWith('mw-')).map(line => {
|
|
39
|
+
const [name, created, attached, windows] = line.split('||');
|
|
40
|
+
return { name, created: new Date(Number(created) * 1000).toISOString(), attached: attached !== '0', windows: Number(windows) || 1 };
|
|
41
|
+
});
|
|
42
|
+
} catch {}
|
|
43
|
+
|
|
44
|
+
// System info
|
|
45
|
+
const uptime = run('uptime');
|
|
46
|
+
const memory = run("ps -o rss= -p $$ 2>/dev/null || echo 0");
|
|
47
|
+
|
|
48
|
+
return NextResponse.json({
|
|
49
|
+
processes: {
|
|
50
|
+
nextjs: { running: nextjs.count > 0, pid: nextjs.pid },
|
|
51
|
+
terminal: { running: terminal.count > 0, pid: terminal.pid },
|
|
52
|
+
telegram: { running: telegram.count > 0, pid: telegram.pid },
|
|
53
|
+
tunnel: { running: tunnel.count > 0, pid: tunnel.pid, url: tunnelUrl },
|
|
54
|
+
},
|
|
55
|
+
sessions,
|
|
56
|
+
uptime: uptime.replace(/.*up\s+/, '').replace(/,\s+\d+ user.*/, '').trim(),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# check-forge-status.sh — Show Forge process status
|
|
3
|
+
|
|
4
|
+
echo "══════════════════════════════════"
|
|
5
|
+
echo " Forge Process Status"
|
|
6
|
+
echo "══════════════════════════════════"
|
|
7
|
+
|
|
8
|
+
# Next.js
|
|
9
|
+
count=$(ps aux | grep 'next-server' | grep -v grep | wc -l | tr -d ' ')
|
|
10
|
+
pid=$(ps aux | grep 'next-server' | grep -v grep | awk '{print $2}' | head -1)
|
|
11
|
+
if [ "$count" -gt 0 ]; then
|
|
12
|
+
echo " ● Next.js running (pid: $pid)"
|
|
13
|
+
else
|
|
14
|
+
echo " ○ Next.js stopped"
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# Terminal
|
|
18
|
+
count=$(ps aux | grep 'terminal-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | wc -l | tr -d ' ')
|
|
19
|
+
pid=$(ps aux | grep 'terminal-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | awk '{print $2}' | head -1)
|
|
20
|
+
if [ "$count" -gt 0 ]; then
|
|
21
|
+
echo " ● Terminal running (pid: $pid)"
|
|
22
|
+
else
|
|
23
|
+
echo " ○ Terminal stopped"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Telegram
|
|
27
|
+
count=$(ps aux | grep 'telegram-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | wc -l | tr -d ' ')
|
|
28
|
+
pid=$(ps aux | grep 'telegram-standalone' | grep -v grep | grep -v 'npm exec' | grep -v 'cli.mjs' | awk '{print $2}' | head -1)
|
|
29
|
+
if [ "$count" -gt 0 ]; then
|
|
30
|
+
echo " ● Telegram running (pid: $pid)"
|
|
31
|
+
else
|
|
32
|
+
echo " ○ Telegram stopped"
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Cloudflare Tunnel
|
|
36
|
+
count=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | wc -l | tr -d ' ')
|
|
37
|
+
pid=$(ps aux | grep 'cloudflared tunnel' | grep -v grep | awk '{print $2}' | head -1)
|
|
38
|
+
url=$(cat ~/.forge/tunnel-state.json 2>/dev/null | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('url',''))" 2>/dev/null)
|
|
39
|
+
if [ "$count" -gt 0 ]; then
|
|
40
|
+
echo " ● Tunnel running (pid: $pid) ${url}"
|
|
41
|
+
else
|
|
42
|
+
echo " ○ Tunnel stopped"
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# tmux sessions
|
|
46
|
+
tmux_count=$(tmux list-sessions 2>/dev/null | grep '^mw-' | wc -l | tr -d ' ')
|
|
47
|
+
echo ""
|
|
48
|
+
echo " Terminal sessions: $tmux_count"
|
|
49
|
+
tmux list-sessions 2>/dev/null | grep '^mw-' | while read line; do
|
|
50
|
+
echo " $line"
|
|
51
|
+
done
|
|
52
|
+
|
|
53
|
+
echo "══════════════════════════════════"
|
package/components/Dashboard.tsx
CHANGED
|
@@ -8,6 +8,7 @@ import SessionView from './SessionView';
|
|
|
8
8
|
import NewTaskModal from './NewTaskModal';
|
|
9
9
|
import SettingsModal from './SettingsModal';
|
|
10
10
|
import TunnelToggle from './TunnelToggle';
|
|
11
|
+
import MonitorPanel from './MonitorPanel';
|
|
11
12
|
import type { Task } from '@/src/types';
|
|
12
13
|
import type { WebTerminalHandle } from './WebTerminal';
|
|
13
14
|
|
|
@@ -44,6 +45,7 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
44
45
|
const [activeTaskId, setActiveTaskId] = useState<string | null>(null);
|
|
45
46
|
const [showNewTask, setShowNewTask] = useState(false);
|
|
46
47
|
const [showSettings, setShowSettings] = useState(false);
|
|
48
|
+
const [showMonitor, setShowMonitor] = useState(false);
|
|
47
49
|
const [usage, setUsage] = useState<UsageSummary[]>([]);
|
|
48
50
|
const [providers, setProviders] = useState<ProviderInfo[]>([]);
|
|
49
51
|
const [projects, setProjects] = useState<ProjectInfo[]>([]);
|
|
@@ -194,6 +196,12 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
194
196
|
)}
|
|
195
197
|
</span>
|
|
196
198
|
)}
|
|
199
|
+
<button
|
|
200
|
+
onClick={() => setShowMonitor(true)}
|
|
201
|
+
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
202
|
+
>
|
|
203
|
+
Monitor
|
|
204
|
+
</button>
|
|
197
205
|
<button
|
|
198
206
|
onClick={() => setShowSettings(true)}
|
|
199
207
|
className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]"
|
|
@@ -365,6 +373,8 @@ export default function Dashboard({ user }: { user: any }) {
|
|
|
365
373
|
/>
|
|
366
374
|
)}
|
|
367
375
|
|
|
376
|
+
{showMonitor && <MonitorPanel onClose={() => setShowMonitor(false)} />}
|
|
377
|
+
|
|
368
378
|
{showSettings && (
|
|
369
379
|
<SettingsModal onClose={() => { setShowSettings(false); fetchData(); }} />
|
|
370
380
|
)}
|
|
@@ -22,6 +22,13 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
|
|
|
22
22
|
const [connected, setConnected] = useState(false);
|
|
23
23
|
const wsRef = useRef<WebSocket | null>(null);
|
|
24
24
|
const docRootRef = useRef(docRoot);
|
|
25
|
+
const skipPermRef = useRef(false);
|
|
26
|
+
|
|
27
|
+
useEffect(() => {
|
|
28
|
+
fetch('/api/settings').then(r => r.json())
|
|
29
|
+
.then((s: any) => { if (s.skipPermissions) skipPermRef.current = true; })
|
|
30
|
+
.catch(() => {});
|
|
31
|
+
}, []);
|
|
25
32
|
docRootRef.current = docRoot;
|
|
26
33
|
|
|
27
34
|
useEffect(() => {
|
|
@@ -78,7 +85,8 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
|
|
|
78
85
|
isNewSession = false;
|
|
79
86
|
setTimeout(() => {
|
|
80
87
|
if (socket.readyState === WebSocket.OPEN) {
|
|
81
|
-
|
|
88
|
+
const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : '';
|
|
89
|
+
socket.send(JSON.stringify({ type: 'input', data: `cd "${docRootRef.current}" && claude --resume${sf}\n` }));
|
|
82
90
|
}
|
|
83
91
|
}, 300);
|
|
84
92
|
}
|
|
@@ -147,13 +155,13 @@ export default function DocTerminal({ docRoot }: { docRoot: string }) {
|
|
|
147
155
|
</span>
|
|
148
156
|
<div className="ml-auto flex items-center gap-1">
|
|
149
157
|
<button
|
|
150
|
-
onClick={() => runCommand(`cd "${docRoot}" && claude`)}
|
|
158
|
+
onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && claude${sf}`); }}
|
|
151
159
|
className="text-[10px] px-2 py-0.5 text-[var(--accent)] hover:bg-[#2a2a4a] rounded"
|
|
152
160
|
>
|
|
153
161
|
New
|
|
154
162
|
</button>
|
|
155
163
|
<button
|
|
156
|
-
onClick={() => runCommand(`cd "${docRoot}" && claude --resume`)}
|
|
164
|
+
onClick={() => { const sf = skipPermRef.current ? ' --dangerously-skip-permissions' : ''; runCommand(`cd "${docRoot}" && claude --resume${sf}`); }}
|
|
157
165
|
className="text-[10px] px-2 py-0.5 text-gray-400 hover:text-white hover:bg-[#2a2a4a] rounded"
|
|
158
166
|
>
|
|
159
167
|
Resume
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState, useEffect, useCallback } from 'react';
|
|
4
|
+
|
|
5
|
+
interface MonitorData {
|
|
6
|
+
processes: {
|
|
7
|
+
nextjs: { running: boolean; pid: string };
|
|
8
|
+
terminal: { running: boolean; pid: string };
|
|
9
|
+
telegram: { running: boolean; pid: string };
|
|
10
|
+
tunnel: { running: boolean; pid: string; url: string };
|
|
11
|
+
};
|
|
12
|
+
sessions: { name: string; created: string; attached: boolean; windows: number }[];
|
|
13
|
+
uptime: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function MonitorPanel({ onClose }: { onClose: () => void }) {
|
|
17
|
+
const [data, setData] = useState<MonitorData | null>(null);
|
|
18
|
+
|
|
19
|
+
const refresh = useCallback(() => {
|
|
20
|
+
fetch('/api/monitor').then(r => r.json()).then(setData).catch(() => {});
|
|
21
|
+
}, []);
|
|
22
|
+
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
refresh();
|
|
25
|
+
const timer = setInterval(refresh, 5000);
|
|
26
|
+
return () => clearInterval(timer);
|
|
27
|
+
}, [refresh]);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50" onClick={onClose}>
|
|
31
|
+
<div className="bg-[var(--bg-secondary)] border border-[var(--border)] rounded-lg w-[500px] max-h-[80vh] overflow-y-auto shadow-xl" onClick={e => e.stopPropagation()}>
|
|
32
|
+
<div className="px-4 py-3 border-b border-[var(--border)] flex items-center justify-between">
|
|
33
|
+
<h2 className="text-sm font-bold text-[var(--text-primary)]">Monitor</h2>
|
|
34
|
+
<div className="flex items-center gap-2">
|
|
35
|
+
<button onClick={refresh} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">↻</button>
|
|
36
|
+
<button onClick={onClose} className="text-[10px] text-[var(--text-secondary)] hover:text-[var(--text-primary)]">Close</button>
|
|
37
|
+
</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
{data ? (
|
|
41
|
+
<div className="p-4 space-y-4">
|
|
42
|
+
{/* Processes */}
|
|
43
|
+
<div>
|
|
44
|
+
<h3 className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-2">Processes</h3>
|
|
45
|
+
<div className="space-y-1.5">
|
|
46
|
+
{[
|
|
47
|
+
{ label: 'Next.js', ...data.processes.nextjs },
|
|
48
|
+
{ label: 'Terminal Server', ...data.processes.terminal },
|
|
49
|
+
{ label: 'Telegram Bot', ...data.processes.telegram },
|
|
50
|
+
{ label: 'Tunnel', ...data.processes.tunnel },
|
|
51
|
+
].map(p => (
|
|
52
|
+
<div key={p.label} className="flex items-center gap-2 text-xs">
|
|
53
|
+
<span className={p.running ? 'text-green-400' : 'text-gray-500'}>●</span>
|
|
54
|
+
<span className="text-[var(--text-primary)] w-28">{p.label}</span>
|
|
55
|
+
{p.running ? (
|
|
56
|
+
<span className="text-[var(--text-secondary)] font-mono text-[10px]">pid: {p.pid}</span>
|
|
57
|
+
) : (
|
|
58
|
+
<span className="text-gray-500 text-[10px]">stopped</span>
|
|
59
|
+
)}
|
|
60
|
+
</div>
|
|
61
|
+
))}
|
|
62
|
+
{data.processes.tunnel.running && data.processes.tunnel.url && (
|
|
63
|
+
<div className="pl-6 text-[10px] text-[var(--accent)] truncate">{data.processes.tunnel.url}</div>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{/* Uptime */}
|
|
69
|
+
{data.uptime && (
|
|
70
|
+
<div className="text-[10px] text-[var(--text-secondary)]">
|
|
71
|
+
Uptime: {data.uptime}
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
|
|
75
|
+
{/* Sessions */}
|
|
76
|
+
<div>
|
|
77
|
+
<h3 className="text-[10px] font-semibold text-[var(--text-secondary)] uppercase mb-2">
|
|
78
|
+
Terminal Sessions ({data.sessions.length})
|
|
79
|
+
</h3>
|
|
80
|
+
{data.sessions.length === 0 ? (
|
|
81
|
+
<div className="text-[10px] text-[var(--text-secondary)]">No sessions</div>
|
|
82
|
+
) : (
|
|
83
|
+
<div className="space-y-1">
|
|
84
|
+
{data.sessions.map(s => (
|
|
85
|
+
<div key={s.name} className="flex items-center gap-2 text-[11px]">
|
|
86
|
+
<span className={s.attached ? 'text-green-400' : 'text-yellow-500'}>●</span>
|
|
87
|
+
<span className="font-mono text-[var(--text-primary)] truncate flex-1">{s.name}</span>
|
|
88
|
+
<span className="text-[9px] text-[var(--text-secondary)]">{s.attached ? 'attached' : 'detached'}</span>
|
|
89
|
+
<span className="text-[9px] text-[var(--text-secondary)]">{new Date(s.created).toLocaleTimeString()}</span>
|
|
90
|
+
</div>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
) : (
|
|
97
|
+
<div className="p-8 text-center text-xs text-[var(--text-secondary)]">Loading...</div>
|
|
98
|
+
)}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
@@ -42,6 +42,7 @@ interface Settings {
|
|
|
42
42
|
taskModel: string;
|
|
43
43
|
pipelineModel: string;
|
|
44
44
|
telegramModel: string;
|
|
45
|
+
skipPermissions: boolean;
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
interface TunnelStatus {
|
|
@@ -66,6 +67,7 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
66
67
|
taskModel: 'sonnet',
|
|
67
68
|
pipelineModel: 'sonnet',
|
|
68
69
|
telegramModel: 'sonnet',
|
|
70
|
+
skipPermissions: false,
|
|
69
71
|
});
|
|
70
72
|
const [newRoot, setNewRoot] = useState('');
|
|
71
73
|
const [newDocRoot, setNewDocRoot] = useState('');
|
|
@@ -347,6 +349,22 @@ export default function SettingsModal({ onClose }: { onClose: () => void }) {
|
|
|
347
349
|
</div>
|
|
348
350
|
</div>
|
|
349
351
|
|
|
352
|
+
{/* Permissions */}
|
|
353
|
+
<div className="space-y-2">
|
|
354
|
+
<label className="flex items-center gap-2 text-xs text-[var(--text-primary)] cursor-pointer">
|
|
355
|
+
<input
|
|
356
|
+
type="checkbox"
|
|
357
|
+
checked={settings.skipPermissions || false}
|
|
358
|
+
onChange={e => setSettings({ ...settings, skipPermissions: e.target.checked })}
|
|
359
|
+
className="rounded"
|
|
360
|
+
/>
|
|
361
|
+
Skip permissions check (--dangerously-skip-permissions)
|
|
362
|
+
</label>
|
|
363
|
+
<p className="text-[9px] text-[var(--text-secondary)]">
|
|
364
|
+
When enabled, all Claude Code tasks and pipelines run without permission prompts. Useful for background automation but less safe.
|
|
365
|
+
</p>
|
|
366
|
+
</div>
|
|
367
|
+
|
|
350
368
|
{/* Remote Access (Cloudflare Tunnel) */}
|
|
351
369
|
<div className="space-y-2">
|
|
352
370
|
<label className="text-xs text-[var(--text-secondary)] font-semibold uppercase">
|
|
@@ -182,6 +182,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
182
182
|
const [showNewTabModal, setShowNewTabModal] = useState(false);
|
|
183
183
|
const [projectRoots, setProjectRoots] = useState<string[]>([]);
|
|
184
184
|
const [allProjects, setAllProjects] = useState<{ name: string; path: string; root: string }[]>([]);
|
|
185
|
+
const [skipPermissions, setSkipPermissions] = useState(false);
|
|
185
186
|
const [expandedRoot, setExpandedRoot] = useState<string | null>(null);
|
|
186
187
|
|
|
187
188
|
// Restore shared state from server after mount
|
|
@@ -196,6 +197,10 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
196
197
|
}
|
|
197
198
|
setHydrated(true);
|
|
198
199
|
});
|
|
200
|
+
// Fetch settings for skipPermissions
|
|
201
|
+
fetch('/api/settings').then(r => r.json())
|
|
202
|
+
.then((s: any) => { if (s.skipPermissions) setSkipPermissions(true); })
|
|
203
|
+
.catch(() => {});
|
|
199
204
|
// Fetch projects and derive roots
|
|
200
205
|
fetch('/api/projects').then(r => r.json())
|
|
201
206
|
.then((p: { name: string; path: string; root: string }[]) => {
|
|
@@ -765,6 +770,7 @@ const WebTerminal = forwardRef<WebTerminalHandle, WebTerminalProps>(function Web
|
|
|
765
770
|
setRatios={tab.id === activeTabId ? setRatios : () => {}}
|
|
766
771
|
onSessionConnected={onSessionConnected}
|
|
767
772
|
refreshKeys={refreshKeys}
|
|
773
|
+
skipPermissions={skipPermissions}
|
|
768
774
|
/>
|
|
769
775
|
</div>
|
|
770
776
|
))}
|
|
@@ -777,7 +783,7 @@ export default WebTerminal;
|
|
|
777
783
|
// ─── Pane renderer ───────────────────────────────────────────
|
|
778
784
|
|
|
779
785
|
function PaneRenderer({
|
|
780
|
-
node, activeId, onFocus, ratios, setRatios, onSessionConnected, refreshKeys,
|
|
786
|
+
node, activeId, onFocus, ratios, setRatios, onSessionConnected, refreshKeys, skipPermissions,
|
|
781
787
|
}: {
|
|
782
788
|
node: SplitNode;
|
|
783
789
|
activeId: number;
|
|
@@ -786,11 +792,12 @@ function PaneRenderer({
|
|
|
786
792
|
setRatios: React.Dispatch<React.SetStateAction<Record<number, number>>>;
|
|
787
793
|
onSessionConnected: (paneId: number, sessionName: string) => void;
|
|
788
794
|
refreshKeys: Record<number, number>;
|
|
795
|
+
skipPermissions?: boolean;
|
|
789
796
|
}) {
|
|
790
797
|
if (node.type === 'terminal') {
|
|
791
798
|
return (
|
|
792
799
|
<div className={`h-full w-full ${activeId === node.id ? 'ring-1 ring-[#7c5bf0]/50 ring-inset' : ''}`} onMouseDown={() => onFocus(node.id)}>
|
|
793
|
-
<MemoTerminalPane key={`${node.id}-${refreshKeys[node.id] || 0}`} id={node.id} sessionName={node.sessionName} projectPath={node.projectPath} onSessionConnected={onSessionConnected} />
|
|
800
|
+
<MemoTerminalPane key={`${node.id}-${refreshKeys[node.id] || 0}`} id={node.id} sessionName={node.sessionName} projectPath={node.projectPath} skipPermissions={skipPermissions} onSessionConnected={onSessionConnected} />
|
|
794
801
|
</div>
|
|
795
802
|
);
|
|
796
803
|
}
|
|
@@ -799,8 +806,8 @@ function PaneRenderer({
|
|
|
799
806
|
|
|
800
807
|
return (
|
|
801
808
|
<DraggableSplit splitId={node.id} direction={node.direction} ratio={ratio} setRatios={setRatios}>
|
|
802
|
-
<PaneRenderer node={node.first} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} />
|
|
803
|
-
<PaneRenderer node={node.second} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} />
|
|
809
|
+
<PaneRenderer node={node.first} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} skipPermissions={skipPermissions} />
|
|
810
|
+
<PaneRenderer node={node.second} activeId={activeId} onFocus={onFocus} ratios={ratios} setRatios={setRatios} onSessionConnected={onSessionConnected} refreshKeys={refreshKeys} skipPermissions={skipPermissions} />
|
|
804
811
|
</DraggableSplit>
|
|
805
812
|
);
|
|
806
813
|
}
|
|
@@ -921,17 +928,21 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
921
928
|
id,
|
|
922
929
|
sessionName,
|
|
923
930
|
projectPath,
|
|
931
|
+
skipPermissions,
|
|
924
932
|
onSessionConnected,
|
|
925
933
|
}: {
|
|
926
934
|
id: number;
|
|
927
935
|
sessionName?: string;
|
|
928
936
|
projectPath?: string;
|
|
937
|
+
skipPermissions?: boolean;
|
|
929
938
|
onSessionConnected: (paneId: number, sessionName: string) => void;
|
|
930
939
|
}) {
|
|
931
940
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
932
941
|
const sessionNameRef = useRef(sessionName);
|
|
933
942
|
sessionNameRef.current = sessionName;
|
|
934
943
|
const projectPathRef = useRef(projectPath);
|
|
944
|
+
const skipPermRef = useRef(skipPermissions);
|
|
945
|
+
skipPermRef.current = skipPermissions;
|
|
935
946
|
projectPathRef.current = projectPath;
|
|
936
947
|
|
|
937
948
|
useEffect(() => {
|
|
@@ -1055,7 +1066,8 @@ const MemoTerminalPane = memo(function TerminalPane({
|
|
|
1055
1066
|
isNewlyCreated = false;
|
|
1056
1067
|
setTimeout(() => {
|
|
1057
1068
|
if (!disposed && ws?.readyState === WebSocket.OPEN) {
|
|
1058
|
-
|
|
1069
|
+
const skipFlag = skipPermRef.current ? ' --dangerously-skip-permissions' : '';
|
|
1070
|
+
ws.send(JSON.stringify({ type: 'input', data: `cd "${projectPathRef.current}" && claude --resume${skipFlag}\n` }));
|
|
1059
1071
|
}
|
|
1060
1072
|
}, 300);
|
|
1061
1073
|
}
|
package/lib/settings.ts
CHANGED
|
@@ -19,6 +19,7 @@ export interface Settings {
|
|
|
19
19
|
taskModel: string; // Model for tasks (default: sonnet)
|
|
20
20
|
pipelineModel: string; // Model for pipelines (default: sonnet)
|
|
21
21
|
telegramModel: string; // Model for Telegram AI features (default: sonnet)
|
|
22
|
+
skipPermissions: boolean; // Add --dangerously-skip-permissions to all claude invocations
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
const defaults: Settings = {
|
|
@@ -34,6 +35,7 @@ const defaults: Settings = {
|
|
|
34
35
|
taskModel: 'default',
|
|
35
36
|
pipelineModel: 'default',
|
|
36
37
|
telegramModel: 'sonnet',
|
|
38
|
+
skipPermissions: false,
|
|
37
39
|
};
|
|
38
40
|
|
|
39
41
|
export function loadSettings(): Settings {
|
package/lib/telegram-bot.ts
CHANGED
|
@@ -16,11 +16,11 @@ import { startTunnel, stopTunnel, getTunnelStatus } from './cloudflared';
|
|
|
16
16
|
import { getPassword } from './password';
|
|
17
17
|
import type { Task, TaskLogEntry } from '@/src/types';
|
|
18
18
|
|
|
19
|
-
//
|
|
19
|
+
// Persist state across hot-reloads
|
|
20
20
|
const globalKey = Symbol.for('mw-telegram-state');
|
|
21
21
|
const g = globalThis as any;
|
|
22
|
-
if (!g[globalKey]) g[globalKey] = {
|
|
23
|
-
const botState: {
|
|
22
|
+
if (!g[globalKey]) g[globalKey] = { taskListenerAttached: false, processedMsgIds: new Set<number>() };
|
|
23
|
+
const botState: { taskListenerAttached: boolean; processedMsgIds: Set<number> } = g[globalKey];
|
|
24
24
|
|
|
25
25
|
// Track which Telegram message maps to which task (for reply-based interaction)
|
|
26
26
|
const taskMessageMap = new Map<number, string>(); // messageId → taskId
|
package/next-env.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/// <reference types="next" />
|
|
2
2
|
/// <reference types="next/image-types/global" />
|
|
3
|
-
import "./.next/
|
|
3
|
+
import "./.next/types/routes.d.ts";
|
|
4
4
|
|
|
5
5
|
// NOTE: This file should not be edited
|
|
6
6
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
package/package.json
CHANGED