claude-simple 0.2.6 → 0.2.8
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/README.md +2 -0
- package/dist/app.0.2.8.min.js +1080 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.8.min.css +1598 -0
- package/gateway.js +0 -0
- package/package.json +1 -1
- package/public/addon-fit.min.js +0 -0
- package/public/agent-panel.js +245 -1
- package/public/api.js +4 -0
- package/public/app.js +0 -0
- package/public/base.css +0 -0
- package/public/cache.js +0 -0
- package/public/chat.js +118 -17
- package/public/files.js +0 -0
- package/public/git.js +0 -0
- package/public/icons/icon-180.png +0 -0
- package/public/icons/icon-192.png +0 -0
- package/public/icons/icon-512.png +0 -0
- package/public/icons/icon-maskable-512.png +0 -0
- package/public/index.html +2 -2
- package/public/lib.js +0 -0
- package/public/manifest.webmanifest +0 -0
- package/public/mobile.js +0 -0
- package/public/notifications.js +215 -0
- package/public/offline.html +0 -0
- package/public/pwa.js +0 -0
- package/public/router.js +0 -0
- package/public/settings.js +0 -0
- package/public/shell/boot.js +0 -0
- package/public/shell/dashboard.js +72 -23
- package/public/shell/hub.js +0 -0
- package/public/shell/layout.js +41 -12
- package/public/shell/notes.js +0 -0
- package/public/shell/session-context.js +0 -0
- package/public/shell/session-list.js +27 -28
- package/public/shell/session-nav.js +36 -1
- package/public/shell/templates.js +45 -7
- package/public/shell/util.js +0 -0
- package/public/shell.js +56 -7
- package/public/state.js +53 -13
- package/public/style.css +100 -6
- package/public/sw.js +2 -2
- package/public/terminal.js +77 -3
- package/public/wake-lock.js +0 -0
- package/public/xterm.min.css +0 -0
- package/public/xterm.min.js +0 -0
- package/src/agents/claude.js +1 -1
- package/src/agents/codex.js +1 -0
- package/src/agents/grok.js +180 -298
- package/src/agents/meta-agent.js +332 -1
- package/src/agents/models.js +0 -0
- package/src/agents/sessions.js +106 -26
- package/src/agents/stream-batch.js +0 -0
- package/src/app.js +88 -11
- package/src/auto-update.js +0 -0
- package/src/runtime/approvals.js +0 -0
- package/src/runtime/outbox.js +0 -0
- package/src/runtime/protocol.js +0 -0
- package/dist/app.0.2.6.min.js +0 -953
- package/dist/style.0.2.6.min.css +0 -1504
|
@@ -10,11 +10,12 @@ import { getLastSessionContext, setLastSessionContext } from './session-context.
|
|
|
10
10
|
import {
|
|
11
11
|
connectWS, clearMessages, appendMsg, appendSystemMsg, renderToolUse,
|
|
12
12
|
appendHistoryTokenBar, appendContextBar, flushToolBatch, coerceTs,
|
|
13
|
+
stashComposerDraft, restoreComposerDraft, discardComposerAttachments, parkQueuedMessages, detachProcessingForSessionSwitch,
|
|
13
14
|
} from '../chat.js';
|
|
14
15
|
import { setHash, syncHash } from '../router.js';
|
|
15
16
|
import { refreshModelSelect } from './session-list.js';
|
|
16
17
|
import { renderSessionNotesBar } from './notes.js';
|
|
17
|
-
import { updateDashboard, getDashboardSessions } from './dashboard.js';
|
|
18
|
+
import { addRecentSession, updateDashboard, getDashboardSessions } from './dashboard.js';
|
|
18
19
|
|
|
19
20
|
/** Show which edge machine Files/Git/Shell paths belong to. */
|
|
20
21
|
function syncMachinePathHints(machineId) {
|
|
@@ -34,6 +35,10 @@ function syncMachinePathHints(machineId) {
|
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
export function openProject(project) {
|
|
38
|
+
stashComposerDraft();
|
|
39
|
+
parkQueuedMessages();
|
|
40
|
+
discardComposerAttachments();
|
|
41
|
+
detachProcessingForSessionSwitch();
|
|
37
42
|
const path = project?.path || '';
|
|
38
43
|
batch(() => {
|
|
39
44
|
currentProject.value = project;
|
|
@@ -56,11 +61,16 @@ export function openProject(project) {
|
|
|
56
61
|
pv.classList.remove('hidden');
|
|
57
62
|
pv.style.display = 'flex';
|
|
58
63
|
clearMessages();
|
|
64
|
+
restoreComposerDraft({ sessionId: null });
|
|
59
65
|
connectWS();
|
|
60
66
|
switchTab('chat');
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export function goHome() {
|
|
70
|
+
stashComposerDraft();
|
|
71
|
+
parkQueuedMessages();
|
|
72
|
+
discardComposerAttachments();
|
|
73
|
+
detachProcessingForSessionSwitch();
|
|
64
74
|
currentProject.value = null;
|
|
65
75
|
sessionFilter.value = null;
|
|
66
76
|
filesRoot.value = '';
|
|
@@ -68,6 +78,7 @@ export function goHome() {
|
|
|
68
78
|
filesPath.value = '';
|
|
69
79
|
viewingFile.value = null;
|
|
70
80
|
ctx.sessionId = null;
|
|
81
|
+
restoreComposerDraft({ sessionId: null });
|
|
71
82
|
syncMachinePathHints(null);
|
|
72
83
|
|
|
73
84
|
// Leaving session restore mode — calendar is allowed again
|
|
@@ -116,10 +127,20 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
116
127
|
const resolvedCwd = cwd || fromList?.cwd || fromLast?.cwd || null;
|
|
117
128
|
const resolvedConfig = configDir || fromList?.configDir || fromLast?.configDir || null;
|
|
118
129
|
|
|
130
|
+
// Capture before changing ctx, then restore only after the new identity is set.
|
|
131
|
+
stashComposerDraft();
|
|
132
|
+
parkQueuedMessages();
|
|
133
|
+
discardComposerAttachments();
|
|
134
|
+
detachProcessingForSessionSwitch();
|
|
135
|
+
|
|
119
136
|
if (resolvedMachine && resolvedMachine !== ctx.machineId) {
|
|
120
137
|
setSelectedMachine(resolvedMachine);
|
|
121
138
|
try { ctx.ws?.close(); } catch {}
|
|
122
139
|
ctx.ws = null;
|
|
140
|
+
// A recent entry can belong to another edge. Route the session request
|
|
141
|
+
// immediately, then refresh the surrounding machine UI in the background.
|
|
142
|
+
import('./hub.js').then(({ syncTopbarMachine }) => syncTopbarMachine()).catch(() => {});
|
|
143
|
+
import('./session-list.js').then(({ loadAllSessions }) => loadAllSessions()).catch(() => {});
|
|
123
144
|
}
|
|
124
145
|
|
|
125
146
|
ctx.sessionId = sid;
|
|
@@ -127,6 +148,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
127
148
|
ctx.agent = resolvedAgent;
|
|
128
149
|
setAgent(resolvedAgent);
|
|
129
150
|
refreshModelSelect();
|
|
151
|
+
restoreComposerDraft({ sessionId: sid, agent: resolvedAgent, machineId: resolvedMachine });
|
|
130
152
|
|
|
131
153
|
// Default path = session cwd on the target machine (set immediately; no await)
|
|
132
154
|
let edgeCwd = resolvedCwd;
|
|
@@ -161,6 +183,19 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
161
183
|
machineId: resolvedMachine,
|
|
162
184
|
tab: targetTab,
|
|
163
185
|
});
|
|
186
|
+
// Recent is a deliberate browsing history, not an automatic list of every
|
|
187
|
+
// session on the currently selected machine. Keep the target machine in
|
|
188
|
+
// the entry so a later click can route back to the right edge.
|
|
189
|
+
addRecentSession({
|
|
190
|
+
sessionId: sid,
|
|
191
|
+
cwd: edgeCwd || resolvedCwd,
|
|
192
|
+
configDir: resolvedConfig,
|
|
193
|
+
agent: resolvedAgent,
|
|
194
|
+
machineId: resolvedMachine,
|
|
195
|
+
display: fromList?.display || fromLast?.display || '',
|
|
196
|
+
projectName: fromList?.projectName || fromLast?.projectName || '',
|
|
197
|
+
updatedAt: fromList?.updatedAt || fromLast?.updatedAt || null,
|
|
198
|
+
});
|
|
164
199
|
|
|
165
200
|
const label = AGENT_LABELS[resolvedAgent] || resolvedAgent;
|
|
166
201
|
const cacheKey = `${resolvedMachine || ''}:${resolvedAgent}:${sid}`;
|
|
@@ -39,10 +39,10 @@ export const AppShell = (opts = {}) => {
|
|
|
39
39
|
: 'hidden flex-col flex-1 overflow-hidden min-h-0';
|
|
40
40
|
const projectStyle = restore ? 'display:flex;flex-direction:column;flex:1;min-height:0' : '';
|
|
41
41
|
return `
|
|
42
|
-
<div id="sidebar-overlay" class="hidden fixed inset-0 bg-black/50 z-[9]"></div>
|
|
42
|
+
<div id="sidebar-overlay" class="hidden fixed inset-0 bg-black/50 z-[9]" aria-hidden="true"></div>
|
|
43
43
|
<div id="app" class="${appHidden} flex-col flex-1 overflow-hidden" style="${appStyle}">
|
|
44
44
|
<div id="topbar" class="flex items-center gap-2 px-3 bg-base-200 border-b border-base-300 flex-shrink-0" style="height:44px">
|
|
45
|
-
<button id="hamburger" class="btn btn-ghost btn-sm px-2 text-lg">☰</button>
|
|
45
|
+
<button id="hamburger" class="btn btn-ghost btn-sm px-2 text-lg" aria-label="打开项目与会话侧栏" aria-controls="sidebar" aria-expanded="false">☰</button>
|
|
46
46
|
<span id="btn-home" class="font-semibold text-sm hidden sm:inline cursor-pointer hover:text-primary transition-colors select-none">🤖 Agent UI</span>
|
|
47
47
|
<span class="text-base-300 hidden sm:inline">/</span>
|
|
48
48
|
<div id="machine-menu-wrap" class="relative hidden flex-shrink-0">
|
|
@@ -69,6 +69,13 @@ export const AppShell = (opts = {}) => {
|
|
|
69
69
|
<span class="text-[10px] opacity-50">▾</span>
|
|
70
70
|
</button>
|
|
71
71
|
</div>
|
|
72
|
+
<div id="notifications-wrap" class="relative flex-shrink-0">
|
|
73
|
+
<button type="button" id="btn-notifications" class="btn btn-ghost btn-xs border border-base-300 gap-1 min-h-8 px-2"
|
|
74
|
+
title="通知" aria-haspopup="dialog" aria-expanded="false">
|
|
75
|
+
<span>🔔</span>
|
|
76
|
+
<span id="notifications-badge" class="hidden inline-flex min-w-5 h-5 px-1 items-center justify-center rounded-full bg-error text-error-content text-[10px] font-bold">0</span>
|
|
77
|
+
</button>
|
|
78
|
+
</div>
|
|
72
79
|
<span id="topbar-project" class="text-sm text-base-content/50 flex-1 truncate min-w-0">Select a session</span>
|
|
73
80
|
<span id="conn-dot" class="w-2 h-2 rounded-full bg-base-content/30 flex-shrink-0" title="连接状态"></span>
|
|
74
81
|
<div class="flex gap-2 topbar-actions">
|
|
@@ -93,6 +100,17 @@ export const AppShell = (opts = {}) => {
|
|
|
93
100
|
<div id="recent-menu-list" class="flex flex-col gap-0.5"></div>
|
|
94
101
|
<p id="recent-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无最近会话</p>
|
|
95
102
|
</div>
|
|
103
|
+
<div id="notifications-backdrop" class="hidden fixed inset-0 z-[55] bg-black/40" aria-hidden="true"></div>
|
|
104
|
+
<div id="notifications-menu" class="hidden fixed z-[60] w-[min(26rem,calc(100vw-1rem))] max-h-[min(30rem,75vh)] overflow-y-auto
|
|
105
|
+
rounded-lg border border-base-300 bg-base-100 shadow-xl p-2"
|
|
106
|
+
role="dialog" aria-label="通知中心">
|
|
107
|
+
<div class="flex items-center justify-between px-1 pb-1.5 mb-1 border-b border-base-300 sticky top-0 bg-base-100 z-[1]">
|
|
108
|
+
<span class="text-[10px] uppercase tracking-wide text-base-content/45 font-semibold">通知</span>
|
|
109
|
+
<button type="button" id="btn-notifications-read-all" class="btn btn-ghost btn-xs px-1 text-[10px]">全部已读</button>
|
|
110
|
+
</div>
|
|
111
|
+
<div id="notifications-menu-list" class="flex flex-col gap-1"></div>
|
|
112
|
+
<p id="notifications-menu-empty" class="text-xs text-base-content/40 px-1 py-2 hidden">暂无通知</p>
|
|
113
|
+
</div>
|
|
96
114
|
<!-- Hub: only when no remembered machine (or last one offline) -->
|
|
97
115
|
<div id="machine-picker" class="hidden fixed inset-0 z-[60] bg-base-100 flex items-center justify-center p-4">
|
|
98
116
|
<div class="w-full max-w-md">
|
|
@@ -107,7 +125,7 @@ export const AppShell = (opts = {}) => {
|
|
|
107
125
|
</div>
|
|
108
126
|
</div>
|
|
109
127
|
<div class="flex flex-1 overflow-hidden relative">
|
|
110
|
-
<
|
|
128
|
+
<aside id="sidebar" class="bg-base-200 border-r border-base-300 flex flex-col flex-shrink-0 overflow-hidden" style="width:280px;min-width:180px;max-width:520px" aria-label="项目与会话" tabindex="-1">
|
|
111
129
|
<div class="flex items-center justify-between px-3 py-2 border-b border-base-300 flex-shrink-0">
|
|
112
130
|
<span class="text-xs uppercase tracking-wider text-base-content/50 font-medium">Projects</span>
|
|
113
131
|
<div class="flex items-center gap-1">
|
|
@@ -143,10 +161,16 @@ export const AppShell = (opts = {}) => {
|
|
|
143
161
|
<div id="search-status" class="hidden text-[10px] text-base-content/40 px-0.5"></div>
|
|
144
162
|
</div>
|
|
145
163
|
<div id="session-list" class="overflow-y-auto flex-1"></div>
|
|
146
|
-
|
|
164
|
+
<div class="sidebar-mobile-actions hidden border-t border-base-300 px-2 py-2 flex-shrink-0">
|
|
165
|
+
<button type="button" class="btn btn-primary btn-sm flex-1" data-sidebar-action="new">+ 新会话</button>
|
|
166
|
+
<button type="button" class="btn btn-ghost btn-sm px-2" data-sidebar-action="theme" aria-label="切换深色或浅色主题">🌙</button>
|
|
167
|
+
<button type="button" class="btn btn-ghost btn-sm px-2" data-sidebar-action="settings" aria-label="设置">⚙️</button>
|
|
168
|
+
<button type="button" class="btn btn-ghost btn-sm px-2 text-error" data-sidebar-action="logout" aria-label="退出登录">⇥</button>
|
|
169
|
+
</div>
|
|
170
|
+
</aside>
|
|
147
171
|
<div class="flex flex-col flex-shrink-0 relative" style="width:12px">
|
|
148
172
|
<div id="sidebar-resize" class="absolute inset-0 bg-transparent hover:bg-primary/30 transition-colors cursor-col-resize"></div>
|
|
149
|
-
<button id="sidebar-toggle" title="
|
|
173
|
+
<button id="sidebar-toggle" title="收起侧栏" aria-label="收起侧栏" aria-controls="sidebar" aria-expanded="true"
|
|
150
174
|
class="absolute z-10 flex items-center justify-center w-5 h-8 bg-base-200 border border-base-300 rounded-r-md text-[10px] text-base-content/40 hover:text-base-content cursor-pointer"
|
|
151
175
|
style="left:0;top:50%;transform:translateY(-50%)">◀</button>
|
|
152
176
|
</div>
|
|
@@ -278,7 +302,7 @@ export const AppShell = (opts = {}) => {
|
|
|
278
302
|
<option value="acceptEdits">acceptEdits</option>
|
|
279
303
|
<option value="auto">auto</option>
|
|
280
304
|
<option value="plan">plan</option>
|
|
281
|
-
<option value="bypassPermissions">⚠ bypass all</option>
|
|
305
|
+
<option value="bypassPermissions" selected>⚠ bypass all</option>
|
|
282
306
|
</select>
|
|
283
307
|
<select id="sel-density" class="select select-xs select-bordered text-xs" title="Chat density — how tools are shown">
|
|
284
308
|
<option value="clean">density: clean</option>
|
|
@@ -332,9 +356,23 @@ export const AppShell = (opts = {}) => {
|
|
|
332
356
|
<div id="tab-shell" class="hidden flex-col flex-1 overflow-hidden bg-[#0d1117]">
|
|
333
357
|
<div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 bg-base-200 flex-shrink-0">
|
|
334
358
|
<span class="text-xs text-base-content/40 font-mono flex-1" id="term-cwd"></span>
|
|
359
|
+
<button id="term-keyboard" class="btn btn-ghost btn-xs text-xs text-base-content/60" type="button">Keyboard</button>
|
|
335
360
|
<button id="term-reconnect" class="btn btn-ghost btn-xs text-xs text-base-content/40">Reconnect</button>
|
|
336
361
|
</div>
|
|
362
|
+
<div id="term-mobile-keys" class="hidden items-center gap-1 px-2 py-1 border-b border-slate-700 bg-[#161b22] flex-shrink-0" aria-label="Terminal shortcuts">
|
|
363
|
+
<button class="term-key" type="button" data-term-key="ctrl-c">Ctrl+C</button>
|
|
364
|
+
<button class="term-key" type="button" data-term-key="escape">Esc</button>
|
|
365
|
+
<button class="term-key" type="button" data-term-key="tab">Tab</button>
|
|
366
|
+
<button class="term-key" type="button" data-term-key="up">↑</button>
|
|
367
|
+
<button class="term-key" type="button" data-term-key="down">↓</button>
|
|
368
|
+
<button class="term-key" type="button" data-term-key="left">←</button>
|
|
369
|
+
<button class="term-key" type="button" data-term-key="right">→</button>
|
|
370
|
+
</div>
|
|
337
371
|
<div id="terminal-container" class="flex-1 p-2 overflow-hidden"></div>
|
|
372
|
+
<form id="term-mobile-composer" class="hidden items-center gap-2 px-2 py-2 border-t border-slate-700 bg-[#161b22] flex-shrink-0">
|
|
373
|
+
<input id="term-mobile-input" class="term-mobile-input" type="text" placeholder="输入命令…" autocomplete="off" autocapitalize="off" autocorrect="off" spellcheck="false" inputmode="text" enterkeyhint="send" aria-label="Terminal command">
|
|
374
|
+
<button class="term-mobile-send" type="submit">发送</button>
|
|
375
|
+
</form>
|
|
338
376
|
</div>
|
|
339
377
|
<div id="tab-git" class="hidden flex-col flex-1 overflow-hidden">
|
|
340
378
|
<div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
|
|
@@ -348,7 +386,7 @@ export const AppShell = (opts = {}) => {
|
|
|
348
386
|
</div>
|
|
349
387
|
<div id="tab-memory" class="hidden flex-col flex-1 overflow-hidden">
|
|
350
388
|
<div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
|
|
351
|
-
<span class="text-xs text-base-content/40 flex-shrink-0">🧠
|
|
389
|
+
<span class="text-xs text-base-content/40 flex-shrink-0">🧠 Agent memory</span>
|
|
352
390
|
<span class="flex-1"></span>
|
|
353
391
|
<button id="memory-refresh" class="btn btn-ghost btn-xs text-xs">Refresh</button>
|
|
354
392
|
</div>
|
package/public/shell/util.js
CHANGED
|
File without changes
|
package/public/shell.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
sessionsData, workspacesData, sessionFilter, sessionSearch,
|
|
7
7
|
expandedFolders, collapsedFolders, filesPath, viewingFile, filteredSessions, projectGroups,
|
|
8
8
|
currentProject, currentTab, filesRoot, gitRoot,
|
|
9
|
-
currentModel, currentEffort, currentPermission, currentAgent, setAgent,
|
|
9
|
+
currentModel, currentEffort, currentPermission, currentAgent, setAgent, setCurrentModel,
|
|
10
10
|
sidebarView, agentFilter, timeRange, activityHits, activityLoading,
|
|
11
11
|
chatDensity, setChatDensity,
|
|
12
12
|
favoritesOnly, showHiddenOnly, hubMode, hubMachineReady, selectedMachineId, setSelectedMachine,
|
|
@@ -23,16 +23,20 @@ import {
|
|
|
23
23
|
import { syncHash } from './router.js';
|
|
24
24
|
import { initSettings, openSettings } from './settings.js';
|
|
25
25
|
import { openMetaAgent, toggleMetaAgent } from './agent-panel.js';
|
|
26
|
+
import {
|
|
27
|
+
initNotifications, refreshNotifications, toggleNotificationsMenu, closeNotificationsMenu,
|
|
28
|
+
} from './notifications.js';
|
|
26
29
|
|
|
27
30
|
import { AuthScreen, AppShell } from './shell/templates.js';
|
|
28
31
|
import {
|
|
29
32
|
updateDashboard, shiftCalMonth, setSelectedCalDate, getDashboardSessions,
|
|
30
33
|
openRecentMenu, toggleRecentMenu, closeRecentMenu, positionRecentMenu,
|
|
34
|
+
dismissRecentSession,
|
|
31
35
|
} from './shell/dashboard.js';
|
|
32
36
|
import {
|
|
33
37
|
loadAllSessions, loadWorkspaces, renderSessionList, syncSidebarChrome,
|
|
34
38
|
refreshModelSelect, refreshEffortSelect, promptCustomModel,
|
|
35
|
-
scheduleActivitySearch, runActivitySearch, loadMemoryTab,
|
|
39
|
+
scheduleActivitySearch, runActivitySearch, loadMemoryTab, openMemoryFile,
|
|
36
40
|
} from './shell/session-list.js';
|
|
37
41
|
import { openProject, goHome, resumeSession, switchTab } from './shell/session-nav.js';
|
|
38
42
|
import { showApp } from './shell/boot.js';
|
|
@@ -305,6 +309,7 @@ export function initShell(opts = {}) {
|
|
|
305
309
|
|
|
306
310
|
initSidebarResize();
|
|
307
311
|
initSidebarToggle();
|
|
312
|
+
initNotifications({ openSession: resumeSession });
|
|
308
313
|
|
|
309
314
|
let sessionsChangedTimer = null;
|
|
310
315
|
document.addEventListener('sessions-changed', () => {
|
|
@@ -336,6 +341,13 @@ export function initShell(opts = {}) {
|
|
|
336
341
|
delegate.on('keydown', '#auth-username, #auth-password', e => { if (e.key === 'Enter') submitAuth(); });
|
|
337
342
|
delegate.on('click', '#hamburger', () => $('sidebar').classList.contains('open') ? closeSidebar() : openSidebar());
|
|
338
343
|
delegate.on('click', '#sidebar-overlay', closeSidebar);
|
|
344
|
+
document.addEventListener('keydown', e => {
|
|
345
|
+
if (e.key !== 'Escape') return;
|
|
346
|
+
if (window.innerWidth <= 640 && $('sidebar')?.classList.contains('open')) {
|
|
347
|
+
e.preventDefault();
|
|
348
|
+
closeSidebar();
|
|
349
|
+
}
|
|
350
|
+
});
|
|
339
351
|
|
|
340
352
|
delegate.on('input', '#session-search', (_, el) => {
|
|
341
353
|
sessionSearch.value = el.value;
|
|
@@ -383,6 +395,19 @@ export function initShell(opts = {}) {
|
|
|
383
395
|
delegate.on('click', '#btn-meta-agent', () => toggleMetaAgent());
|
|
384
396
|
delegate.on('click', '#btn-meta-agent-welcome', () => openMetaAgent('chat'));
|
|
385
397
|
delegate.on('click', '#btn-theme', toggleTheme);
|
|
398
|
+
delegate.on('click', '[data-sidebar-action]', (_, el) => {
|
|
399
|
+
const action = el.dataset.sidebarAction;
|
|
400
|
+
if (action === 'new') openNewSessionModal();
|
|
401
|
+
if (action === 'theme') toggleTheme();
|
|
402
|
+
if (action === 'settings') openSettings();
|
|
403
|
+
if (action === 'logout') {
|
|
404
|
+
ctx.token = null;
|
|
405
|
+
localStorage.removeItem('token');
|
|
406
|
+
setSelectedMachine(null);
|
|
407
|
+
hubMachineReady.value = false;
|
|
408
|
+
location.reload();
|
|
409
|
+
}
|
|
410
|
+
});
|
|
386
411
|
|
|
387
412
|
delegate.on('click', '#btn-cal-prev', () => {
|
|
388
413
|
shiftCalMonth(-1);
|
|
@@ -462,6 +487,7 @@ export function initShell(opts = {}) {
|
|
|
462
487
|
});
|
|
463
488
|
|
|
464
489
|
delegate.on('click', '#memory-refresh', loadMemoryTab);
|
|
490
|
+
delegate.on('click', '[data-memory-file]', (el) => openMemoryFile(el.dataset.memoryFile, el.dataset.memoryAgent));
|
|
465
491
|
delegate.on('click', '[data-tab]', (e, el) => switchTab(el.dataset.tab));
|
|
466
492
|
|
|
467
493
|
const applyRoot = async (inputId, sig) => {
|
|
@@ -497,8 +523,7 @@ export function initShell(opts = {}) {
|
|
|
497
523
|
if (prev !== next) {
|
|
498
524
|
ctx.sessionId = null;
|
|
499
525
|
setAgent(next);
|
|
500
|
-
|
|
501
|
-
localStorage.setItem('model', currentModel.peek());
|
|
526
|
+
setCurrentModel(getDefaultModel(next) || currentModel.peek());
|
|
502
527
|
$('sel-permission').value = currentPermission.peek();
|
|
503
528
|
refreshModelSelect();
|
|
504
529
|
appendSystemMsg(`Agent → ${AGENT_LABELS[next] || next} · model ${currentModel.peek()} · new session`);
|
|
@@ -532,8 +557,7 @@ export function initShell(opts = {}) {
|
|
|
532
557
|
}
|
|
533
558
|
});
|
|
534
559
|
delegate.on('change', '#sel-model', (_, el) => {
|
|
535
|
-
|
|
536
|
-
localStorage.setItem('model', el.value);
|
|
560
|
+
setCurrentModel(el.value);
|
|
537
561
|
refreshEffortSelect();
|
|
538
562
|
});
|
|
539
563
|
delegate.on('click', '#btn-model-add', () => promptCustomModel({ editCurrent: false }));
|
|
@@ -568,6 +592,7 @@ export function initShell(opts = {}) {
|
|
|
568
592
|
return;
|
|
569
593
|
}
|
|
570
594
|
await enterMachine(id, { forceReload: true });
|
|
595
|
+
refreshNotifications();
|
|
571
596
|
});
|
|
572
597
|
const refreshMachinesUi = async () => {
|
|
573
598
|
const st = $('machine-picker-status');
|
|
@@ -589,6 +614,7 @@ export function initShell(opts = {}) {
|
|
|
589
614
|
delegate.on('click', '#btn-machine-menu', async e => {
|
|
590
615
|
e.stopPropagation();
|
|
591
616
|
closeRecentMenu();
|
|
617
|
+
closeNotificationsMenu();
|
|
592
618
|
const menu = $('machine-menu');
|
|
593
619
|
if (!menu) return;
|
|
594
620
|
if (menu.classList.contains('hidden')) {
|
|
@@ -603,7 +629,8 @@ export function initShell(opts = {}) {
|
|
|
603
629
|
e.preventDefault();
|
|
604
630
|
e.stopPropagation();
|
|
605
631
|
closeMachineMenu();
|
|
606
|
-
|
|
632
|
+
closeNotificationsMenu();
|
|
633
|
+
toggleRecentMenu();
|
|
607
634
|
const open = !$('recent-menu')?.classList.contains('hidden');
|
|
608
635
|
const btn = $('btn-recent-sessions');
|
|
609
636
|
if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
@@ -627,6 +654,13 @@ export function initShell(opts = {}) {
|
|
|
627
654
|
btn?.setAttribute('aria-expanded', 'false');
|
|
628
655
|
}
|
|
629
656
|
});
|
|
657
|
+
delegate.on('click', '#btn-notifications', e => {
|
|
658
|
+
e.preventDefault();
|
|
659
|
+
e.stopPropagation();
|
|
660
|
+
closeMachineMenu();
|
|
661
|
+
closeRecentMenu();
|
|
662
|
+
toggleNotificationsMenu();
|
|
663
|
+
});
|
|
630
664
|
// Keep panel glued under topbar on rotate / URL bar show-hide
|
|
631
665
|
window.addEventListener('resize', () => {
|
|
632
666
|
if ($('recent-menu') && !$('recent-menu').classList.contains('hidden')) {
|
|
@@ -638,6 +672,15 @@ export function initShell(opts = {}) {
|
|
|
638
672
|
closeRecentMenu();
|
|
639
673
|
$('btn-recent-sessions')?.setAttribute('aria-expanded', 'false');
|
|
640
674
|
});
|
|
675
|
+
delegate.on('click', '[data-dismiss-recent-session]', (e, el) => {
|
|
676
|
+
e.preventDefault();
|
|
677
|
+
e.stopPropagation();
|
|
678
|
+
dismissRecentSession({
|
|
679
|
+
sessionId: el.dataset.dismissRecentSession,
|
|
680
|
+
agent: el.dataset.dismissRecentAgent || 'claude',
|
|
681
|
+
machineId: el.dataset.dismissRecentMachine || null,
|
|
682
|
+
});
|
|
683
|
+
});
|
|
641
684
|
|
|
642
685
|
delegate.on('click', '#btn-edit-project-notes, #project-goal-text, #project-notes-text', () => {
|
|
643
686
|
renderProjectNotesEditMode();
|
|
@@ -730,6 +773,12 @@ export function initShell(opts = {}) {
|
|
|
730
773
|
});
|
|
731
774
|
|
|
732
775
|
refreshModelSelect();
|
|
776
|
+
effect(() => {
|
|
777
|
+
void hubMode.value;
|
|
778
|
+
void selectedMachineId.value;
|
|
779
|
+
if (!ctx.token) return;
|
|
780
|
+
refreshNotifications();
|
|
781
|
+
});
|
|
733
782
|
$('sel-permission').value = currentPermission.peek();
|
|
734
783
|
applyChatDensity();
|
|
735
784
|
const dens = $('sel-density');
|
package/public/state.js
CHANGED
|
@@ -15,14 +15,52 @@ export const currentTab = signal('chat');
|
|
|
15
15
|
export const isProcessing = signal(false);
|
|
16
16
|
/** Chat socket: idle | connecting | open | reconnecting | offline */
|
|
17
17
|
export const wsStatus = signal('idle');
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
const MODEL_BY_SESSION_KEY = 'modelBySession';
|
|
19
|
+
const initialAgent = localStorage.getItem('agent') || 'claude';
|
|
20
|
+
|
|
21
|
+
function readModelBySession() {
|
|
22
|
+
try {
|
|
23
|
+
const value = JSON.parse(localStorage.getItem(MODEL_BY_SESSION_KEY) || '{}');
|
|
24
|
+
return value && typeof value === 'object' ? value : {};
|
|
25
|
+
} catch {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function modelSessionKey(agent, sessionId = ctx.sessionId, machineId = ctx.machineId) {
|
|
31
|
+
if (!sessionId) return '';
|
|
32
|
+
const a = (agent || 'claude').toLowerCase();
|
|
33
|
+
return `${machineId || ''}:${a}:${sessionId}`;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Restore the model explicitly selected for the active session. */
|
|
37
|
+
export function getSavedModel(agent, sessionId = ctx.sessionId, machineId = ctx.machineId) {
|
|
38
|
+
const key = modelSessionKey(agent, sessionId, machineId);
|
|
39
|
+
if (!key) return null;
|
|
40
|
+
const saved = readModelBySession()[key];
|
|
41
|
+
if (typeof saved === 'string' && saved.trim()) return saved;
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Update the picker and persist its value only when a session exists. */
|
|
46
|
+
export function setCurrentModel(model, agent = currentAgent.peek(), sessionId = ctx.sessionId, machineId = ctx.machineId) {
|
|
47
|
+
const value = String(model || '').trim();
|
|
48
|
+
if (!value) return;
|
|
49
|
+
const key = modelSessionKey(agent, sessionId, machineId);
|
|
50
|
+
if (key) {
|
|
51
|
+
const saved = readModelBySession();
|
|
52
|
+
saved[key] = value;
|
|
53
|
+
localStorage.setItem(MODEL_BY_SESSION_KEY, JSON.stringify(saved));
|
|
54
|
+
}
|
|
55
|
+
currentModel.value = value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const currentAgent = signal(initialAgent); // 'claude'|'codex'|'grok'
|
|
59
|
+
export const currentModel = signal('claude-sonnet-4-5');
|
|
20
60
|
export const currentEffort = signal('');
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
export const currentPermission = signal(
|
|
24
|
-
currentAgent.peek() === 'codex' ? 'bypassPermissions' : 'default'
|
|
25
|
-
); // 'default'|'acceptEdits'|'bypassPermissions'|'plan'|'auto'
|
|
61
|
+
// All local agents are trusted by default; users can still choose another mode
|
|
62
|
+
// from the expanded composer options before starting a turn.
|
|
63
|
+
export const currentPermission = signal('bypassPermissions'); // 'default'|'acceptEdits'|'bypassPermissions'|'plan'|'auto'
|
|
26
64
|
export const filesRoot = signal(''); // custom root override for Files tab
|
|
27
65
|
export const gitRoot = signal(''); // custom root override for Git tab
|
|
28
66
|
export const agentsMeta = signal(null); // from /api/agents (dynamic model lists)
|
|
@@ -251,14 +289,16 @@ export function setAgent(agent) {
|
|
|
251
289
|
const a = (agent || 'claude').toLowerCase();
|
|
252
290
|
currentAgent.value = a;
|
|
253
291
|
localStorage.setItem('agent', a);
|
|
254
|
-
currentPermission.value =
|
|
292
|
+
currentPermission.value = 'bypassPermissions';
|
|
255
293
|
const permissionSelect = typeof document === 'undefined' ? null : document.getElementById('sel-permission');
|
|
256
294
|
if (permissionSelect) permissionSelect.value = currentPermission.peek();
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
295
|
+
// Model selection belongs to a concrete session, not to the whole agent.
|
|
296
|
+
// New sessions start from the agent default until they receive a session id.
|
|
297
|
+
const saved = getSavedModel(a);
|
|
298
|
+
if (saved) {
|
|
299
|
+
currentModel.value = saved;
|
|
300
|
+
} else {
|
|
301
|
+
setCurrentModel(getDefaultModel(a), a);
|
|
262
302
|
}
|
|
263
303
|
}
|
|
264
304
|
|
package/public/style.css
CHANGED
|
@@ -6,21 +6,25 @@
|
|
|
6
6
|
top: calc(44px + env(safe-area-inset-top, 0px));
|
|
7
7
|
left: 0;
|
|
8
8
|
bottom: 0;
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
width: min(86vw, 22rem) !important;
|
|
10
|
+
min-width: min(86vw, 22rem) !important;
|
|
11
|
+
max-width: min(86vw, 22rem) !important;
|
|
12
|
+
transform: translateX(-103%); z-index: 10;
|
|
13
|
+
transition: transform .22s cubic-bezier(.2,.8,.2,1), box-shadow .22s ease;
|
|
11
14
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
15
|
+
overscroll-behavior: contain;
|
|
12
16
|
}
|
|
13
17
|
#sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,.4); }
|
|
14
18
|
|
|
15
19
|
/* Compact chat composer on phones — less height, less dead space under bar */
|
|
16
20
|
#composer-row { padding: 0 !important; }
|
|
17
|
-
.composer-surface { min-height: 0; border-radius:
|
|
21
|
+
.composer-surface { min-height: 0; border-radius: 0.8rem; }
|
|
18
22
|
#chat-input.chat-input,
|
|
19
23
|
#chat-input {
|
|
20
24
|
min-height: 32px !important;
|
|
21
25
|
max-height: 72px !important;
|
|
22
26
|
padding: 0 !important;
|
|
23
|
-
border-radius:
|
|
27
|
+
border-radius: 0.65rem 0.65rem 0 0;
|
|
24
28
|
/* 16px prevents iOS Safari auto-zoom on focus */
|
|
25
29
|
font-size: 16px !important;
|
|
26
30
|
line-height: 1.4 !important;
|
|
@@ -76,6 +80,25 @@
|
|
|
76
80
|
#sidebar-toggle {
|
|
77
81
|
display: none !important;
|
|
78
82
|
}
|
|
83
|
+
#sidebar .input,
|
|
84
|
+
#sidebar .select {
|
|
85
|
+
min-height: 2.25rem;
|
|
86
|
+
font-size: 16px !important; /* prevents Safari focus zoom */
|
|
87
|
+
}
|
|
88
|
+
#sidebar #time-range { font-size: 12px !important; }
|
|
89
|
+
#sidebar #session-list {
|
|
90
|
+
padding-bottom: max(0.75rem, env(safe-area-inset-bottom, 0px));
|
|
91
|
+
-webkit-overflow-scrolling: touch;
|
|
92
|
+
}
|
|
93
|
+
#sidebar [data-view],
|
|
94
|
+
#sidebar .agent-pill {
|
|
95
|
+
min-height: 28px;
|
|
96
|
+
}
|
|
97
|
+
#sidebar .sidebar-mobile-actions {
|
|
98
|
+
display: flex;
|
|
99
|
+
gap: 0.35rem;
|
|
100
|
+
padding-bottom: max(0.5rem, env(safe-area-inset-bottom, 0px));
|
|
101
|
+
}
|
|
79
102
|
.md {
|
|
80
103
|
font-size: 0.9375rem;
|
|
81
104
|
line-height: 1.6;
|
|
@@ -101,6 +124,16 @@
|
|
|
101
124
|
}
|
|
102
125
|
}
|
|
103
126
|
|
|
127
|
+
@media (min-width: 641px) {
|
|
128
|
+
#sidebar .sidebar-mobile-actions { display: none !important; }
|
|
129
|
+
#sidebar {
|
|
130
|
+
transition: width .18s ease, min-width .18s ease;
|
|
131
|
+
}
|
|
132
|
+
#sidebar.desktop-collapsed {
|
|
133
|
+
border-right-color: transparent;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
104
137
|
/* File row actions: hover-reveal on desktop only */
|
|
105
138
|
@media (min-width: 641px) {
|
|
106
139
|
.file-row-action {
|
|
@@ -962,7 +995,7 @@ html.pwa-standalone .ma-panel {
|
|
|
962
995
|
flex-direction: column;
|
|
963
996
|
overflow: hidden;
|
|
964
997
|
border: 1px solid color-mix(in oklch, var(--color-base-content) 16%, transparent);
|
|
965
|
-
border-radius:
|
|
998
|
+
border-radius: 0.875rem;
|
|
966
999
|
background: color-mix(in oklch, var(--color-base-100) 78%, var(--color-base-200));
|
|
967
1000
|
box-shadow: 0 8px 24px color-mix(in oklch, black 12%, transparent), inset 0 1px 0 color-mix(in oklch, white 6%, transparent);
|
|
968
1001
|
transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
|
|
@@ -1064,7 +1097,7 @@ html.pwa-standalone .ma-panel {
|
|
|
1064
1097
|
max-height: 72px;
|
|
1065
1098
|
padding: 0;
|
|
1066
1099
|
border: 0;
|
|
1067
|
-
border-radius:
|
|
1100
|
+
border-radius: 0.75rem 0.75rem 0 0;
|
|
1068
1101
|
background: transparent;
|
|
1069
1102
|
color: var(--color-base-content);
|
|
1070
1103
|
outline: none !important;
|
|
@@ -1297,6 +1330,52 @@ html.pwa-standalone .ma-panel {
|
|
|
1297
1330
|
/* Shell tab terminal — keep dark intentionally (terminal aesthetic) */
|
|
1298
1331
|
#tab-shell { background: #0d1117; }
|
|
1299
1332
|
#term-output { color: #c9d1d9; }
|
|
1333
|
+
#term-keyboard { display: none; }
|
|
1334
|
+
|
|
1335
|
+
/* xterm parks its input off-screen, which is unreliable for mobile IMEs.
|
|
1336
|
+
* Phones use the visible native command composer below the terminal instead. */
|
|
1337
|
+
@media (max-width: 640px), (hover: none) and (pointer: coarse) {
|
|
1338
|
+
#term-keyboard { display: inline-flex; }
|
|
1339
|
+
#term-mobile-keys { display: flex !important; }
|
|
1340
|
+
#term-mobile-composer { display: flex !important; }
|
|
1341
|
+
#terminal-container { touch-action: manipulation; }
|
|
1342
|
+
.term-key {
|
|
1343
|
+
min-width: 38px;
|
|
1344
|
+
min-height: 32px;
|
|
1345
|
+
padding: 3px 7px;
|
|
1346
|
+
border: 1px solid #30363d;
|
|
1347
|
+
border-radius: 6px;
|
|
1348
|
+
color: #c9d1d9;
|
|
1349
|
+
background: #21262d;
|
|
1350
|
+
font: 600 12px/1 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1351
|
+
-webkit-tap-highlight-color: transparent;
|
|
1352
|
+
}
|
|
1353
|
+
.term-key:active { background: #30363d; }
|
|
1354
|
+
.term-mobile-input {
|
|
1355
|
+
flex: 1;
|
|
1356
|
+
min-width: 0;
|
|
1357
|
+
height: 38px;
|
|
1358
|
+
padding: 0 10px;
|
|
1359
|
+
border: 1px solid #30363d;
|
|
1360
|
+
border-radius: 7px;
|
|
1361
|
+
color: #f0f6fc;
|
|
1362
|
+
background: #0d1117;
|
|
1363
|
+
font: 16px/1.2 ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1364
|
+
outline: none;
|
|
1365
|
+
}
|
|
1366
|
+
.term-mobile-input:focus { border-color: #58a6ff; }
|
|
1367
|
+
.term-mobile-send {
|
|
1368
|
+
height: 38px;
|
|
1369
|
+
padding: 0 12px;
|
|
1370
|
+
border: 0;
|
|
1371
|
+
border-radius: 7px;
|
|
1372
|
+
color: #fff;
|
|
1373
|
+
background: #1f6feb;
|
|
1374
|
+
font-size: 14px;
|
|
1375
|
+
font-weight: 600;
|
|
1376
|
+
}
|
|
1377
|
+
.term-mobile-send:active { background: #388bfd; }
|
|
1378
|
+
}
|
|
1300
1379
|
|
|
1301
1380
|
/* ── Meta Agent panel (DaisyUI 5: use --color-* full values, solid surfaces) ── */
|
|
1302
1381
|
.meta-agent-panel {
|
|
@@ -1343,6 +1422,21 @@ html.pwa-standalone .ma-panel {
|
|
|
1343
1422
|
background: color-mix(in oklch, var(--color-primary) 12%, var(--color-base-200));
|
|
1344
1423
|
}
|
|
1345
1424
|
.ma-hist-open { cursor: pointer; }
|
|
1425
|
+
.ma-note-item {
|
|
1426
|
+
width: 100%;
|
|
1427
|
+
border: 1px solid var(--color-base-300);
|
|
1428
|
+
border-radius: 10px;
|
|
1429
|
+
padding: 8px 10px;
|
|
1430
|
+
background: var(--color-base-200);
|
|
1431
|
+
transition: border-color .12s, background .12s;
|
|
1432
|
+
}
|
|
1433
|
+
.ma-note-item:hover {
|
|
1434
|
+
border-color: color-mix(in oklch, var(--color-primary) 40%, var(--color-base-300));
|
|
1435
|
+
}
|
|
1436
|
+
.ma-note-item.active {
|
|
1437
|
+
border-color: color-mix(in oklch, var(--color-primary) 55%, var(--color-base-300));
|
|
1438
|
+
background: color-mix(in oklch, var(--color-primary) 12%, var(--color-base-200));
|
|
1439
|
+
}
|
|
1346
1440
|
.line-clamp-2 {
|
|
1347
1441
|
display: -webkit-box;
|
|
1348
1442
|
-webkit-line-clamp: 2;
|
package/public/sw.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* Agent UI service worker — cache shell assets only.
|
|
2
2
|
* Never intercepts WebSocket or mutates API semantics beyond offline fallback.
|
|
3
3
|
* Bump CACHE when shipping UI that must invalidate old shells. */
|
|
4
|
-
const CACHE = 'agent-ui-shell-
|
|
4
|
+
const CACHE = 'agent-ui-shell-v15';
|
|
5
5
|
const PRECACHE = [
|
|
6
6
|
'/',
|
|
7
7
|
'/index.html',
|
|
8
8
|
'/manifest.webmanifest',
|
|
9
|
-
'/style.css?v=ui-
|
|
9
|
+
'/style.css?v=ui-11',
|
|
10
10
|
'/style.css',
|
|
11
11
|
'/app.js',
|
|
12
12
|
'/icons/icon-192.png',
|