claude-simple 0.1.0 → 0.2.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/client.js +31 -2
- package/dist/app.0.2.1.min.js +868 -0
- package/dist/highlight.min.js +448 -430
- package/dist/index.html +2 -2
- package/dist/style.0.2.1.min.css +1182 -0
- package/package.json +1 -1
- package/public/agent-panel.js +44 -0
- package/public/api.js +61 -10
- package/public/app.js +4 -0
- package/public/chat.js +338 -31
- package/public/files.js +22 -30
- package/public/git.js +21 -17
- 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 +16 -3
- package/public/lib.js +7 -2
- package/public/manifest.webmanifest +36 -0
- package/public/offline.html +33 -0
- package/public/pwa.js +78 -0
- package/public/shell/boot.js +3 -0
- package/public/shell/hub.js +4 -2
- package/public/shell/notes.js +93 -31
- package/public/shell/session-list.js +29 -10
- package/public/shell/session-nav.js +7 -2
- package/public/shell/templates.js +11 -3
- package/public/shell.js +101 -8
- package/public/state.js +13 -2
- package/public/style.css +135 -3
- package/public/sw.js +104 -0
- package/public/terminal.js +22 -0
- package/public/wake-lock.js +45 -0
- package/server.js +5 -1
- package/src/agents/claude.js +108 -10
- package/src/agents/codex.js +1 -1
- package/src/agents/grok.js +40 -32
- package/src/agents/meta-agent.js +40 -1
- package/src/agents/stream-batch.js +62 -0
- package/src/app.js +312 -41
- package/src/auto-update.js +215 -0
- package/src/runtime/approvals.js +67 -0
- package/src/runtime/outbox.js +143 -0
- package/src/runtime/protocol.js +224 -0
- package/dist/app.0.1.0.min.js +0 -825
- package/dist/style.0.1.0.min.css +0 -1050
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
getModelsForAgent, getDefaultModel, getEffortsForModel,
|
|
11
11
|
addCustomModel, removeCustomModel, getCustomModels, ctx,
|
|
12
12
|
} from '../state.js';
|
|
13
|
-
import { api } from '../api.js';
|
|
13
|
+
import { api, NOT_MODIFIED } from '../api.js';
|
|
14
14
|
import { getCachedSessions, setCachedSessions, getCachedWorkspaces, setCachedWorkspaces } from '../cache.js';
|
|
15
15
|
import { formatTime, agentBadge, agentCountPills, shortPath } from './util.js';
|
|
16
16
|
|
|
@@ -22,6 +22,16 @@ function normalizeSessions(v) {
|
|
|
22
22
|
return [];
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
function sessionsQuery() {
|
|
26
|
+
const days = parseInt(timeRange.peek() || '14', 10);
|
|
27
|
+
const q = new URLSearchParams();
|
|
28
|
+
// Match sidebar time filter so we don't pull years of metadata on mobile.
|
|
29
|
+
if (days > 0) q.set('days', String(days));
|
|
30
|
+
q.set('limit', '400');
|
|
31
|
+
const s = q.toString();
|
|
32
|
+
return s ? `/api/sessions?${s}` : '/api/sessions';
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
/** @param {{ waitFresh?: boolean }} opts waitFresh=false → paint cache first, refresh in bg */
|
|
26
36
|
export async function loadAllSessions({ waitFresh = true } = {}) {
|
|
27
37
|
const cached = normalizeSessions(await getCachedSessions());
|
|
@@ -37,10 +47,13 @@ export async function loadAllSessions({ waitFresh = true } = {}) {
|
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
const fetchFresh = async () => {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
50
|
+
const path = sessionsQuery();
|
|
51
|
+
const fresh = await api('GET', path, undefined, undefined, { etag: true });
|
|
52
|
+
if (fresh === NOT_MODIFIED) return sessionsData.peek();
|
|
53
|
+
const list = normalizeSessions(fresh);
|
|
54
|
+
sessionsData.value = list;
|
|
55
|
+
setCachedSessions(list);
|
|
56
|
+
return list;
|
|
44
57
|
};
|
|
45
58
|
|
|
46
59
|
if (waitFresh || !cached.length) {
|
|
@@ -79,6 +92,7 @@ export function sessionItemHtml(s, { showProject = false } = {}) {
|
|
|
79
92
|
? s.snippet
|
|
80
93
|
: sessionDisplayTitle(s);
|
|
81
94
|
const fav = !!meta.favorite;
|
|
95
|
+
const hid = !!meta.hidden;
|
|
82
96
|
const hasNote = !!(meta.notes && String(meta.notes).trim());
|
|
83
97
|
const turns = Number(s.turnCount) || 0;
|
|
84
98
|
const thin = turns > 0 && turns <= LOW_TURN_THRESHOLD;
|
|
@@ -87,7 +101,8 @@ export function sessionItemHtml(s, { showProject = false } = {}) {
|
|
|
87
101
|
: '';
|
|
88
102
|
const badges = [
|
|
89
103
|
thin ? `<span class="session-thin-badge" title="Only ${turns} turn${turns === 1 ? '' : 's'}">thin·${turns}</span>` : '',
|
|
90
|
-
renamed ? `<span class="session-renamed-badge" title="Custom name (original: ${esc(s.display || '')})"
|
|
104
|
+
renamed ? `<span class="session-renamed-badge" title="Custom name (original: ${esc(s.display || '')})">renamed</span>` : '',
|
|
105
|
+
hid ? `<span class="session-hidden-badge" title="Hidden">hidden</span>` : '',
|
|
91
106
|
hasNote ? `<span class="session-note-dot" title="${esc(meta.notes)}">📝</span>` : '',
|
|
92
107
|
].filter(Boolean).join('');
|
|
93
108
|
const machineBadge = s.machineId
|
|
@@ -97,7 +112,7 @@ export function sessionItemHtml(s, { showProject = false } = {}) {
|
|
|
97
112
|
? `${title}\n(original: ${s.display || s.sessionId})`
|
|
98
113
|
: (s.display || '');
|
|
99
114
|
return `<div class="session-row px-2.5 py-1.5 cursor-pointer hover:bg-base-300 border-l-2
|
|
100
|
-
${active ? 'border-primary bg-primary/5' : 'border-transparent'} ${fav ? 'session-fav' : ''} ${thin ? 'session-thin' : ''}"
|
|
115
|
+
${active ? 'border-primary bg-primary/5' : 'border-transparent'} ${fav ? 'session-fav' : ''} ${hid ? 'session-hidden-row' : ''} ${thin ? 'session-thin' : ''}"
|
|
101
116
|
data-session-id="${esc(s.sessionId)}"
|
|
102
117
|
data-session-cwd="${esc(s.cwd || '')}"
|
|
103
118
|
data-session-config-dir="${esc(s.configDir || '')}"
|
|
@@ -114,17 +129,21 @@ export function sessionItemHtml(s, { showProject = false } = {}) {
|
|
|
114
129
|
${agentBadge(s.agent)}
|
|
115
130
|
<div class="min-w-0 flex-1">
|
|
116
131
|
<div class="session-title-wrap flex items-center gap-1 min-w-0"
|
|
117
|
-
data-session-id="${esc(s.sessionId)}"
|
|
118
132
|
data-rename-session-id="${esc(s.sessionId)}"
|
|
119
133
|
data-rename-session-agent="${esc(s.agent || 'claude')}"
|
|
120
134
|
data-rename-session-machine="${esc(s.machineId || '')}"
|
|
121
135
|
data-rename-session-display="${esc(s.display || '')}">
|
|
122
136
|
<div class="session-title-text text-[11px] leading-snug truncate flex-1 min-w-0 ${active ? 'text-primary font-medium' : 'text-base-content/85'} ${renamed ? 'session-title-renamed' : ''}"
|
|
123
|
-
|
|
124
|
-
title="${esc(tip)} · click to rename">${esc(title)}</div>
|
|
137
|
+
title="${esc(tip)}">${esc(title)}</div>
|
|
125
138
|
<button type="button" class="session-rename-btn flex-shrink-0"
|
|
126
139
|
data-rename-session="1"
|
|
127
140
|
title="Rename session">✎</button>
|
|
141
|
+
<button type="button" class="session-hide-btn flex-shrink-0 ${hid ? 'is-hidden' : ''}"
|
|
142
|
+
data-hide-toggle="1"
|
|
143
|
+
data-hide-session-id="${esc(s.sessionId)}"
|
|
144
|
+
data-hide-session-agent="${esc(s.agent || 'claude')}"
|
|
145
|
+
data-hide-session-machine="${esc(s.machineId || '')}"
|
|
146
|
+
title="${hid ? 'Unhide session' : 'Hide session'}">${hid ? '👁' : '🙈'}</button>
|
|
128
147
|
</div>
|
|
129
148
|
${projectLine}
|
|
130
149
|
${badges || machineBadge ? `<div class="flex items-center gap-1 mt-0.5 flex-wrap">${machineBadge}${badges}</div>` : ''}
|
|
@@ -217,8 +217,13 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
217
217
|
}
|
|
218
218
|
appendSystemMsg(`Loading ${label} history…`);
|
|
219
219
|
try {
|
|
220
|
-
// tail
|
|
221
|
-
const
|
|
220
|
+
// Smaller tail + compact tool payloads on mobile / Save-Data.
|
|
221
|
+
const saveData = typeof navigator !== 'undefined' && (
|
|
222
|
+
navigator.connection?.saveData
|
|
223
|
+
|| /Mobi|Android|iPhone/i.test(navigator.userAgent || '')
|
|
224
|
+
);
|
|
225
|
+
const tail = saveData ? '50' : '80';
|
|
226
|
+
const q = new URLSearchParams({ agent: resolvedAgent, tail, compact: '1' });
|
|
222
227
|
const data = await api('GET', `/api/sessions/${sid}/messages?${q}`);
|
|
223
228
|
const msgs = Array.isArray(data) ? data : (data.messages || []);
|
|
224
229
|
const context = Array.isArray(data) ? null : (data.context || null);
|
|
@@ -39,7 +39,7 @@ export const AppShell = (opts = {}) => {
|
|
|
39
39
|
return `
|
|
40
40
|
<div id="sidebar-overlay" class="hidden fixed inset-0 bg-black/50 z-[9]"></div>
|
|
41
41
|
<div id="app" class="${appHidden} flex-col flex-1 overflow-hidden" style="${appStyle}">
|
|
42
|
-
<div class="flex items-center gap-2 px-3 bg-base-200 border-b border-base-300 flex-shrink-0" style="height:44px">
|
|
42
|
+
<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">
|
|
43
43
|
<button id="hamburger" class="btn btn-ghost btn-sm px-2 text-lg">☰</button>
|
|
44
44
|
<span id="btn-home" class="font-semibold text-sm hidden sm:inline cursor-pointer hover:text-primary transition-colors select-none">🤖 Agent UI</span>
|
|
45
45
|
<span class="text-base-300 hidden sm:inline">/</span>
|
|
@@ -61,7 +61,10 @@ export const AppShell = (opts = {}) => {
|
|
|
61
61
|
</div>
|
|
62
62
|
</div>
|
|
63
63
|
<span id="topbar-project" class="text-sm text-base-content/50 flex-1 truncate">Select a session</span>
|
|
64
|
+
<span id="conn-dot" class="w-2 h-2 rounded-full bg-base-content/30 flex-shrink-0" title="连接状态"></span>
|
|
64
65
|
<div class="flex gap-2">
|
|
66
|
+
<button id="btn-refresh-conn" class="btn btn-ghost btn-xs border border-base-300" title="重新连接并同步">↻</button>
|
|
67
|
+
<button id="btn-pwa-install" class="btn btn-ghost btn-xs border border-base-300 hidden" title="安装到主屏幕">安装</button>
|
|
65
68
|
<button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary" title="Meta Agent — 报告 / 问答 / VLM">✦ Agent</button>
|
|
66
69
|
<button id="btn-new-session" class="btn btn-ghost btn-xs border border-base-300">+ New</button>
|
|
67
70
|
<button id="btn-theme" class="btn btn-ghost btn-xs border border-base-300">🌙</button>
|
|
@@ -114,6 +117,7 @@ export const AppShell = (opts = {}) => {
|
|
|
114
117
|
<button data-agent-filter="codex" class="agent-pill">Codex</button>
|
|
115
118
|
<button data-agent-filter="grok" class="agent-pill">Grok</button>
|
|
116
119
|
<button id="btn-fav-filter" class="agent-pill" title="Show only favorites">★ Fav</button>
|
|
120
|
+
<button id="btn-hidden-filter" class="agent-pill" title="Show only hidden sessions">🙈 Hidden</button>
|
|
117
121
|
</div>
|
|
118
122
|
<div id="search-status" class="hidden text-[10px] text-base-content/40 px-0.5"></div>
|
|
119
123
|
</div>
|
|
@@ -205,13 +209,17 @@ export const AppShell = (opts = {}) => {
|
|
|
205
209
|
<div id="tab-chat" class="flex flex-col flex-1 overflow-hidden relative">
|
|
206
210
|
<div id="project-notes-bar" class="bg-base-200 border-b border-base-300 px-3 py-1.5 text-xs flex flex-col gap-1" style="display:none"></div>
|
|
207
211
|
<div id="session-notes-bar" class="bg-base-200/80 border-b border-base-300 px-3 py-1 text-xs flex justify-between items-center gap-2" style="display:none"></div>
|
|
212
|
+
<div id="ws-banner" class="hidden px-3 py-1.5 text-[11px] text-center bg-warning/15 text-warning border-b border-warning/20 flex items-center justify-center gap-2">
|
|
213
|
+
<span id="ws-banner-text"></span>
|
|
214
|
+
<button type="button" id="ws-banner-retry" class="btn btn-ghost btn-xs h-6 min-h-0 px-2 text-warning border border-warning/30">重连</button>
|
|
215
|
+
</div>
|
|
208
216
|
<div id="messages" class="flex-1 overflow-y-auto p-3 flex flex-col gap-2"></div>
|
|
209
217
|
<div id="scroll-nav" class="absolute right-3 bottom-24 flex flex-col gap-1.5 z-10">
|
|
210
218
|
<button id="scroll-top-btn" class="scroll-fab hidden" title="Jump to top">↑</button>
|
|
211
219
|
<button id="scroll-bottom-btn" class="scroll-fab hidden" title="Jump to bottom">↓</button>
|
|
212
220
|
</div>
|
|
213
221
|
<div id="permission-requests"></div>
|
|
214
|
-
<div class="flex flex-col border-t border-base-300 bg-base-200 flex-shrink-0">
|
|
222
|
+
<div id="chat-composer" class="flex flex-col border-t border-base-300 bg-base-200 flex-shrink-0">
|
|
215
223
|
<div id="chat-opts" class="hidden flex items-center gap-1.5 px-3 pt-2 flex-wrap">
|
|
216
224
|
<select id="sel-agent" class="select select-xs select-bordered text-xs font-semibold" title="Agent">
|
|
217
225
|
<option value="claude">Claude</option>
|
|
@@ -258,7 +266,7 @@ export const AppShell = (opts = {}) => {
|
|
|
258
266
|
<div class="flex gap-2 items-end px-3 pt-2 pb-2">
|
|
259
267
|
<button id="btn-opts" class="btn btn-ghost btn-sm px-2 text-base-content/30 hover:text-base-content flex-shrink-0" title="Options" style="height:40px">⚙</button>
|
|
260
268
|
<textarea id="chat-input" rows="1" placeholder="Ask… (!cmd shell · ⌘↵ send)"
|
|
261
|
-
class="
|
|
269
|
+
class="chat-input flex-1 text-sm resize-none leading-relaxed"
|
|
262
270
|
style="min-height:40px;max-height:140px"></textarea>
|
|
263
271
|
<button id="send-btn" class="btn btn-primary btn-sm flex-shrink-0" style="height:40px">Send</button>
|
|
264
272
|
<button id="stop-btn" class="btn btn-error btn-sm hidden flex-shrink-0" style="height:40px">■</button>
|
package/public/shell.js
CHANGED
|
@@ -9,10 +9,11 @@ import {
|
|
|
9
9
|
currentModel, currentEffort, currentPermission, currentAgent, setAgent,
|
|
10
10
|
sidebarView, agentFilter, timeRange, activityHits, activityLoading,
|
|
11
11
|
chatDensity, setChatDensity,
|
|
12
|
-
favoritesOnly, hubMode, hubMachineReady, selectedMachineId, setSelectedMachine,
|
|
12
|
+
favoritesOnly, showHiddenOnly, hubMode, hubMachineReady, selectedMachineId, setSelectedMachine,
|
|
13
13
|
machinesList, AGENT_LABELS, getDefaultModel, sessionMetaMap, ctx,
|
|
14
14
|
} from './state.js';
|
|
15
15
|
import { api } from './api.js';
|
|
16
|
+
import { promptPwaInstall } from './pwa.js';
|
|
16
17
|
import { getLastSessionContext } from './shell/session-context.js';
|
|
17
18
|
import {
|
|
18
19
|
connectWS, clearMessages, appendMsg, appendSystemMsg, sendMessage, stopProcessing,
|
|
@@ -34,7 +35,7 @@ import { showApp } from './shell/boot.js';
|
|
|
34
35
|
import {
|
|
35
36
|
fetchProjectNotes, renderProjectNotesDisplayMode, renderProjectNotesEditMode,
|
|
36
37
|
renderSessionNotesBar, renderSessionNotesEditMode,
|
|
37
|
-
toggleSessionFavorite, saveSessionNotes, startInlineRename, setProjectNoteFields,
|
|
38
|
+
toggleSessionFavorite, toggleSessionHidden, saveSessionNotes, startInlineRename, setProjectNoteFields,
|
|
38
39
|
} from './shell/notes.js';
|
|
39
40
|
import {
|
|
40
41
|
refreshMachinesList, renderMachinePickerList, renderMachineMenuList,
|
|
@@ -50,6 +51,48 @@ import {
|
|
|
50
51
|
// Re-export public API (app.js and others)
|
|
51
52
|
export { showApp, resumeSession, switchTab, openProject, goHome, loadAllSessions };
|
|
52
53
|
|
|
54
|
+
/** Visible install guide when browser has no beforeinstallprompt (iOS Safari etc.). */
|
|
55
|
+
function showPwaInstallHelp() {
|
|
56
|
+
const isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent)
|
|
57
|
+
|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
58
|
+
const isAndroid = /Android/i.test(navigator.userAgent);
|
|
59
|
+
let body;
|
|
60
|
+
if (isIOS) {
|
|
61
|
+
body = `<ol class="list-decimal pl-4 space-y-2 text-sm text-base-content/80">
|
|
62
|
+
<li>点底部分享按钮 <strong>分享</strong>(方框+向上箭头)</li>
|
|
63
|
+
<li>下滑找到并点 <strong>添加到主屏幕</strong></li>
|
|
64
|
+
<li>确认名称后点 <strong>添加</strong></li>
|
|
65
|
+
</ol>
|
|
66
|
+
<p class="text-xs text-base-content/50 mt-3">需用 <strong>Safari</strong> 打开本站;微信/Chrome 内置浏览器通常没有「添加到主屏幕」。</p>`;
|
|
67
|
+
} else if (isAndroid) {
|
|
68
|
+
body = `<ol class="list-decimal pl-4 space-y-2 text-sm text-base-content/80">
|
|
69
|
+
<li>点右上角菜单 <strong>⋮</strong></li>
|
|
70
|
+
<li>选择 <strong>安装应用</strong> 或 <strong>添加到主屏幕</strong></li>
|
|
71
|
+
</ol>
|
|
72
|
+
<p class="text-xs text-base-content/50 mt-3">若菜单里没有该项:请用 <strong>Chrome</strong> 打开,并先接受本站证书提示。自签 HTTPS 下部分机型不弹出一键安装。</p>`;
|
|
73
|
+
} else {
|
|
74
|
+
body = `<p class="text-sm text-base-content/80">请在浏览器菜单中选择 <strong>安装应用 / 安装网页应用 / 添加到主屏幕</strong>。</p>
|
|
75
|
+
<p class="text-xs text-base-content/50 mt-3">Chrome / Edge 地址栏右侧有时也会出现安装图标。</p>`;
|
|
76
|
+
}
|
|
77
|
+
let dlg = document.getElementById('modal-pwa-install');
|
|
78
|
+
if (!dlg) {
|
|
79
|
+
dlg = document.createElement('dialog');
|
|
80
|
+
dlg.id = 'modal-pwa-install';
|
|
81
|
+
dlg.className = 'modal';
|
|
82
|
+
document.body.appendChild(dlg);
|
|
83
|
+
}
|
|
84
|
+
dlg.innerHTML = `
|
|
85
|
+
<div class="modal-box max-w-sm">
|
|
86
|
+
<h3 class="font-bold text-base mb-3">安装到主屏幕</h3>
|
|
87
|
+
${body}
|
|
88
|
+
<div class="modal-action">
|
|
89
|
+
<form method="dialog"><button class="btn btn-sm">知道了</button></form>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
<form method="dialog" class="modal-backdrop"><button>close</button></form>`;
|
|
93
|
+
dlg.showModal();
|
|
94
|
+
}
|
|
95
|
+
|
|
53
96
|
export async function initAuth() {
|
|
54
97
|
const authScreen = $('auth-screen');
|
|
55
98
|
if (authScreen) {
|
|
@@ -235,7 +278,15 @@ export function initShell(opts = {}) {
|
|
|
235
278
|
initSidebarResize();
|
|
236
279
|
initSidebarToggle();
|
|
237
280
|
|
|
238
|
-
|
|
281
|
+
let sessionsChangedTimer = null;
|
|
282
|
+
document.addEventListener('sessions-changed', () => {
|
|
283
|
+
// Debounce: rapid agent turns would otherwise re-download the full list.
|
|
284
|
+
if (sessionsChangedTimer) clearTimeout(sessionsChangedTimer);
|
|
285
|
+
sessionsChangedTimer = setTimeout(() => {
|
|
286
|
+
sessionsChangedTimer = null;
|
|
287
|
+
loadAllSessions({ waitFresh: false });
|
|
288
|
+
}, 1200);
|
|
289
|
+
});
|
|
239
290
|
document.addEventListener('router:home', goHome);
|
|
240
291
|
|
|
241
292
|
document.addEventListener('router:session', async ({ detail: { id, tab } }) => {
|
|
@@ -279,6 +330,7 @@ export function initShell(opts = {}) {
|
|
|
279
330
|
delegate.on('change', '#time-range', (_, el) => {
|
|
280
331
|
timeRange.value = el.value;
|
|
281
332
|
localStorage.setItem('timeRange', el.value);
|
|
333
|
+
loadAllSessions({ waitFresh: true });
|
|
282
334
|
if (sessionSearch.peek().trim()) scheduleActivitySearch(sessionSearch.peek());
|
|
283
335
|
});
|
|
284
336
|
delegate.on('dblclick', '[data-project-cwd]', (_, el) => {
|
|
@@ -288,6 +340,16 @@ export function initShell(opts = {}) {
|
|
|
288
340
|
openProject({ id: name, name, path: cwd });
|
|
289
341
|
appendSystemMsg(`Project · ${cwd}`);
|
|
290
342
|
});
|
|
343
|
+
delegate.on('click', '#btn-pwa-install', async () => {
|
|
344
|
+
const ok = await promptPwaInstall();
|
|
345
|
+
if (ok) return;
|
|
346
|
+
// iOS / browsers without beforeinstallprompt: chat system messages are easy
|
|
347
|
+
// to miss (no open session / wrong tab). Always show a modal.
|
|
348
|
+
showPwaInstallHelp();
|
|
349
|
+
});
|
|
350
|
+
document.addEventListener('pwa-installable', () => {
|
|
351
|
+
document.getElementById('btn-pwa-install')?.classList.remove('hidden');
|
|
352
|
+
});
|
|
291
353
|
delegate.on('click', '#btn-settings', openSettings);
|
|
292
354
|
delegate.on('click', '#btn-meta-agent', () => toggleMetaAgent());
|
|
293
355
|
delegate.on('click', '#btn-meta-agent-welcome', () => openMetaAgent('chat'));
|
|
@@ -308,9 +370,11 @@ export function initShell(opts = {}) {
|
|
|
308
370
|
|
|
309
371
|
delegate.on('click', '[data-session-id]', (e, el) => {
|
|
310
372
|
if (e.target.closest('[data-fav-toggle]')) return;
|
|
373
|
+
if (e.target.closest('[data-hide-toggle]')) return;
|
|
311
374
|
if (e.target.closest('[data-rename-session]')) return;
|
|
312
375
|
if (e.target.closest('.session-rename-input')) return;
|
|
313
376
|
if (e.target.closest('.session-title-wrap.session-rename-active')) return;
|
|
377
|
+
// Only open when clicking the row / title — not action buttons
|
|
314
378
|
resumeSession(
|
|
315
379
|
el.dataset.sessionId,
|
|
316
380
|
el.dataset.sessionCwd || null,
|
|
@@ -320,7 +384,7 @@ export function initShell(opts = {}) {
|
|
|
320
384
|
);
|
|
321
385
|
closeSidebarOnMobile();
|
|
322
386
|
});
|
|
323
|
-
// Inline rename (sidebar):
|
|
387
|
+
// Inline rename (sidebar): only the ✎ button — title click opens the session
|
|
324
388
|
delegate.on('click', '[data-rename-session]', (e, el) => {
|
|
325
389
|
e.preventDefault();
|
|
326
390
|
e.stopPropagation();
|
|
@@ -335,12 +399,12 @@ export function initShell(opts = {}) {
|
|
|
335
399
|
mountEl: wrap,
|
|
336
400
|
});
|
|
337
401
|
});
|
|
338
|
-
// Inline rename (session bar title
|
|
339
|
-
delegate.on('click', '[data-rename-bar]
|
|
402
|
+
// Inline rename (session bar): only the ✎ button next to the title
|
|
403
|
+
delegate.on('click', '[data-rename-bar]', (e, el) => {
|
|
340
404
|
e.preventDefault();
|
|
341
405
|
e.stopPropagation();
|
|
342
406
|
if (!ctx.sessionId) return;
|
|
343
|
-
const mountEl =
|
|
407
|
+
const mountEl = document.getElementById('session-title-display') || el;
|
|
344
408
|
startInlineRename({
|
|
345
409
|
sessionId: ctx.sessionId,
|
|
346
410
|
agent: ctx.agent || currentAgent.peek() || 'claude',
|
|
@@ -540,7 +604,18 @@ export function initShell(opts = {}) {
|
|
|
540
604
|
e.preventDefault();
|
|
541
605
|
const mid = el.dataset.favSessionMachine || ctx.machineId;
|
|
542
606
|
if (mid) { ctx.machineId = mid; localStorage.setItem('machineId', mid); }
|
|
543
|
-
toggleSessionFavorite(el.dataset.favSessionId, el.dataset.favSessionAgent || 'claude'
|
|
607
|
+
toggleSessionFavorite(el.dataset.favSessionId, el.dataset.favSessionAgent || 'claude', {
|
|
608
|
+
machineId: mid || null,
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
delegate.on('click', '[data-hide-toggle]', (e, el) => {
|
|
612
|
+
e.stopPropagation();
|
|
613
|
+
e.preventDefault();
|
|
614
|
+
const mid = el.dataset.hideSessionMachine || ctx.machineId;
|
|
615
|
+
if (mid) { ctx.machineId = mid; localStorage.setItem('machineId', mid); }
|
|
616
|
+
toggleSessionHidden(el.dataset.hideSessionId, el.dataset.hideSessionAgent || 'claude', {
|
|
617
|
+
machineId: mid || null,
|
|
618
|
+
});
|
|
544
619
|
});
|
|
545
620
|
delegate.on('click', '#btn-edit-session-notes, #session-notes-text', () => {
|
|
546
621
|
renderSessionNotesEditMode();
|
|
@@ -566,6 +641,23 @@ export function initShell(opts = {}) {
|
|
|
566
641
|
favoritesOnly.value = next;
|
|
567
642
|
localStorage.setItem('favoritesOnly', next ? '1' : '0');
|
|
568
643
|
$('btn-fav-filter')?.classList.toggle('active', next);
|
|
644
|
+
// Fav and Hidden filters are exclusive for clarity
|
|
645
|
+
if (next && showHiddenOnly.peek()) {
|
|
646
|
+
showHiddenOnly.value = false;
|
|
647
|
+
localStorage.setItem('showHiddenOnly', '0');
|
|
648
|
+
$('btn-hidden-filter')?.classList.remove('active');
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
delegate.on('click', '#btn-hidden-filter', () => {
|
|
652
|
+
const next = !showHiddenOnly.peek();
|
|
653
|
+
showHiddenOnly.value = next;
|
|
654
|
+
localStorage.setItem('showHiddenOnly', next ? '1' : '0');
|
|
655
|
+
$('btn-hidden-filter')?.classList.toggle('active', next);
|
|
656
|
+
if (next && favoritesOnly.peek()) {
|
|
657
|
+
favoritesOnly.value = false;
|
|
658
|
+
localStorage.setItem('favoritesOnly', '0');
|
|
659
|
+
$('btn-fav-filter')?.classList.remove('active');
|
|
660
|
+
}
|
|
569
661
|
});
|
|
570
662
|
|
|
571
663
|
refreshModelSelect();
|
|
@@ -578,6 +670,7 @@ export function initShell(opts = {}) {
|
|
|
578
670
|
btn.classList.toggle('active', btn.dataset.agentFilter === agentFilter.peek());
|
|
579
671
|
});
|
|
580
672
|
$('btn-fav-filter')?.classList.toggle('active', favoritesOnly.peek());
|
|
673
|
+
$('btn-hidden-filter')?.classList.toggle('active', showHiddenOnly.peek());
|
|
581
674
|
document.querySelectorAll('[data-view]').forEach(btn => {
|
|
582
675
|
const on = btn.dataset.view === sidebarView.peek();
|
|
583
676
|
btn.classList.toggle('bg-primary/15', on);
|
package/public/state.js
CHANGED
|
@@ -13,6 +13,8 @@ export const viewingFile = signal(null); // currently viewing file in F
|
|
|
13
13
|
export const currentProject = signal(null); // { id, name, path } | null
|
|
14
14
|
export const currentTab = signal('chat');
|
|
15
15
|
export const isProcessing = signal(false);
|
|
16
|
+
/** Chat socket: idle | connecting | open | reconnecting | offline */
|
|
17
|
+
export const wsStatus = signal('idle');
|
|
16
18
|
export const currentAgent = signal(localStorage.getItem('agent') || 'claude'); // 'claude'|'codex'|'grok'
|
|
17
19
|
export const currentModel = signal(localStorage.getItem('model') || 'claude-sonnet-4-5');
|
|
18
20
|
export const currentEffort = signal('');
|
|
@@ -27,10 +29,12 @@ export const agentFilter = signal(localStorage.getItem('agentFilter') || 'all
|
|
|
27
29
|
export const timeRange = signal(localStorage.getItem('timeRange') || '14'); // '0'|'1'|'7'|'14'|'30'
|
|
28
30
|
export const activityHits = signal(null); // null | { q, results, loading, error }
|
|
29
31
|
export const activityLoading = signal(false);
|
|
30
|
-
/** agent:sessionId → { favorite, notes, title, updatedAt } (title = rename map) */
|
|
32
|
+
/** agent:sessionId → { favorite, notes, title, hidden, updatedAt } (title = rename map) */
|
|
31
33
|
export const sessionMetaMap = signal({});
|
|
32
34
|
/** Sidebar: show only favorited sessions */
|
|
33
35
|
export const favoritesOnly = signal(localStorage.getItem('favoritesOnly') === '1');
|
|
36
|
+
/** Sidebar: show only hidden sessions (default list excludes hidden) */
|
|
37
|
+
export const showHiddenOnly = signal(localStorage.getItem('showHiddenOnly') === '1');
|
|
34
38
|
/** Sessions with this many user turns or fewer get a “thin” badge */
|
|
35
39
|
export const LOW_TURN_THRESHOLD = 2;
|
|
36
40
|
|
|
@@ -77,7 +81,7 @@ export function metaKey(agent, sessionId, machineId) {
|
|
|
77
81
|
|
|
78
82
|
export function getSessionMeta(agent, sessionId, machineId) {
|
|
79
83
|
const m = sessionMetaMap.peek()?.[metaKey(agent, sessionId, machineId)];
|
|
80
|
-
return m || { favorite: false, notes: '', title: '' };
|
|
84
|
+
return m || { favorite: false, notes: '', title: '', hidden: false };
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
/** Display title: user rename map → original display → short id. */
|
|
@@ -295,6 +299,7 @@ export const filteredSessions = computed(() => {
|
|
|
295
299
|
const aFilter = agentFilter.value;
|
|
296
300
|
const days = timeRange.value;
|
|
297
301
|
const favOnly = favoritesOnly.value;
|
|
302
|
+
const hiddenOnly = showHiddenOnly.value;
|
|
298
303
|
const metaMap = sessionMetaMap.value;
|
|
299
304
|
// When deep activity results are present and user is searching, prefer those
|
|
300
305
|
const hits = activityHits.value;
|
|
@@ -309,6 +314,12 @@ export const filteredSessions = computed(() => {
|
|
|
309
314
|
list = list.filter(s => s && (!s.machineId || s.machineId === mid));
|
|
310
315
|
}
|
|
311
316
|
list = list.filter(s => s && matchesAgentFilter(s, aFilter) && matchesTimeRange(s, days));
|
|
317
|
+
// Hidden sessions: excluded by default; "Hidden" filter shows only them
|
|
318
|
+
if (hiddenOnly) {
|
|
319
|
+
list = list.filter(s => s && metaMap?.[metaKey(s.agent, s.sessionId, s.machineId)]?.hidden);
|
|
320
|
+
} else {
|
|
321
|
+
list = list.filter(s => s && !metaMap?.[metaKey(s.agent, s.sessionId, s.machineId)]?.hidden);
|
|
322
|
+
}
|
|
312
323
|
if (favOnly) {
|
|
313
324
|
list = list.filter(s => s && metaMap?.[metaKey(s.agent, s.sessionId, s.machineId)]?.favorite);
|
|
314
325
|
}
|
package/public/style.css
CHANGED
|
@@ -2,13 +2,62 @@
|
|
|
2
2
|
@media (min-width: 641px) { #hamburger { display: none !important; } }
|
|
3
3
|
@media (max-width: 640px) {
|
|
4
4
|
#sidebar {
|
|
5
|
-
position: fixed;
|
|
5
|
+
position: fixed;
|
|
6
|
+
top: calc(44px + env(safe-area-inset-top, 0px));
|
|
7
|
+
left: 0;
|
|
8
|
+
bottom: 0;
|
|
6
9
|
transform: translateX(-100%); z-index: 10;
|
|
7
10
|
transition: transform .2s ease;
|
|
11
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
8
12
|
}
|
|
9
13
|
#sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,.4); }
|
|
10
14
|
}
|
|
11
15
|
|
|
16
|
+
/*
|
|
17
|
+
* PWA standalone safe areas:
|
|
18
|
+
* Do NOT pad the whole body (that leaves a dead empty strip under the chat bar).
|
|
19
|
+
* Paint chrome into the insets; only inset interactive content.
|
|
20
|
+
*/
|
|
21
|
+
html, body {
|
|
22
|
+
height: 100%;
|
|
23
|
+
height: 100dvh;
|
|
24
|
+
}
|
|
25
|
+
html.pwa-standalone,
|
|
26
|
+
html.pwa-standalone body {
|
|
27
|
+
background: oklch(var(--b1));
|
|
28
|
+
}
|
|
29
|
+
/* Top bar: extend under status bar, push content below notch */
|
|
30
|
+
html.pwa-standalone #topbar {
|
|
31
|
+
padding-top: env(safe-area-inset-top, 0px);
|
|
32
|
+
min-height: calc(44px + env(safe-area-inset-top, 0px));
|
|
33
|
+
height: auto !important;
|
|
34
|
+
padding-left: max(0.75rem, env(safe-area-inset-left, 0px));
|
|
35
|
+
padding-right: max(0.75rem, env(safe-area-inset-right, 0px));
|
|
36
|
+
}
|
|
37
|
+
/* Chat composer: sit above home indicator (background fills the inset) */
|
|
38
|
+
html.pwa-standalone #chat-composer {
|
|
39
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
40
|
+
padding-left: env(safe-area-inset-left, 0px);
|
|
41
|
+
padding-right: env(safe-area-inset-right, 0px);
|
|
42
|
+
}
|
|
43
|
+
/* Welcome / machine picker full-screen panels */
|
|
44
|
+
html.pwa-standalone #welcome,
|
|
45
|
+
html.pwa-standalone #machine-picker {
|
|
46
|
+
padding-top: max(1rem, env(safe-area-inset-top, 0px));
|
|
47
|
+
padding-bottom: max(1rem, env(safe-area-inset-bottom, 0px));
|
|
48
|
+
padding-left: max(1rem, env(safe-area-inset-left, 0px));
|
|
49
|
+
padding-right: max(1rem, env(safe-area-inset-right, 0px));
|
|
50
|
+
}
|
|
51
|
+
/* Meta agent drawer bottom */
|
|
52
|
+
html.pwa-standalone #meta-agent-panel,
|
|
53
|
+
html.pwa-standalone .ma-panel {
|
|
54
|
+
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#conn-dot {
|
|
58
|
+
box-shadow: 0 0 0 2px color-mix(in oklch, var(--color-base-200) 80%, transparent);
|
|
59
|
+
}
|
|
60
|
+
|
|
12
61
|
/* ── Theme-aware code / tool surfaces ────────────────────────────────────── */
|
|
13
62
|
/* Prefer DaisyUI tokens so light/dark both look intentional — no #0d1117 hardcode. */
|
|
14
63
|
.code-surface {
|
|
@@ -542,23 +591,54 @@
|
|
|
542
591
|
opacity: 0.9;
|
|
543
592
|
}
|
|
544
593
|
.session-title-renamed { font-style: normal; }
|
|
545
|
-
.session-rename-btn
|
|
594
|
+
.session-rename-btn,
|
|
595
|
+
.session-hide-btn {
|
|
596
|
+
appearance: none;
|
|
597
|
+
border: none;
|
|
598
|
+
background: transparent;
|
|
546
599
|
font-size: 10px;
|
|
547
600
|
line-height: 1;
|
|
548
601
|
padding: 1px 3px;
|
|
549
602
|
border-radius: 4px;
|
|
550
603
|
color: color-mix(in oklch, var(--color-base-content) 35%, transparent);
|
|
551
604
|
opacity: 0;
|
|
605
|
+
cursor: pointer;
|
|
552
606
|
transition: opacity .12s, color .12s, background .12s;
|
|
553
607
|
}
|
|
554
608
|
.session-row:hover .session-rename-btn,
|
|
555
|
-
.session-row:focus-within .session-rename-btn
|
|
609
|
+
.session-row:focus-within .session-rename-btn,
|
|
610
|
+
.session-row:hover .session-hide-btn,
|
|
611
|
+
.session-row:focus-within .session-hide-btn,
|
|
612
|
+
#session-notes-bar .session-rename-btn,
|
|
613
|
+
#session-notes-bar .session-hide-btn {
|
|
556
614
|
opacity: 1;
|
|
557
615
|
}
|
|
558
616
|
.session-rename-btn:hover {
|
|
559
617
|
color: var(--color-primary);
|
|
560
618
|
background: color-mix(in oklch, var(--color-primary) 12%, transparent);
|
|
561
619
|
}
|
|
620
|
+
.session-hide-btn:hover {
|
|
621
|
+
color: var(--color-warning);
|
|
622
|
+
background: color-mix(in oklch, var(--color-warning) 12%, transparent);
|
|
623
|
+
}
|
|
624
|
+
.session-hide-btn.is-hidden {
|
|
625
|
+
opacity: 1;
|
|
626
|
+
color: var(--color-warning);
|
|
627
|
+
}
|
|
628
|
+
.session-row.session-hidden-row {
|
|
629
|
+
opacity: 0.78;
|
|
630
|
+
}
|
|
631
|
+
.session-hidden-badge {
|
|
632
|
+
font-size: 9px;
|
|
633
|
+
font-family: ui-monospace, Menlo, monospace;
|
|
634
|
+
font-weight: 700;
|
|
635
|
+
letter-spacing: 0.02em;
|
|
636
|
+
padding: 0 4px;
|
|
637
|
+
border-radius: 3px;
|
|
638
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
639
|
+
background: color-mix(in oklch, var(--color-base-content) 8%, transparent);
|
|
640
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content) 14%, transparent);
|
|
641
|
+
}
|
|
562
642
|
.session-title-wrap {
|
|
563
643
|
width: 100%;
|
|
564
644
|
}
|
|
@@ -747,6 +827,38 @@
|
|
|
747
827
|
.diff-hunk { color: oklch(var(--in)); }
|
|
748
828
|
.diff-head { color: oklch(var(--bc)/.5); }
|
|
749
829
|
|
|
830
|
+
/* ── Chat composer ───────────────────────────────────────────────────────── */
|
|
831
|
+
/* Single outer border only — no DaisyUI nested focus ring / inner outline. */
|
|
832
|
+
#chat-input.chat-input,
|
|
833
|
+
#chat-input {
|
|
834
|
+
width: 100%;
|
|
835
|
+
min-height: 40px;
|
|
836
|
+
max-height: 140px;
|
|
837
|
+
padding: 0.55rem 0.85rem;
|
|
838
|
+
border-radius: 0.75rem;
|
|
839
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content) 16%, transparent);
|
|
840
|
+
background: var(--color-base-100);
|
|
841
|
+
color: var(--color-base-content);
|
|
842
|
+
outline: none !important;
|
|
843
|
+
box-shadow: none !important;
|
|
844
|
+
--tw-ring-shadow: 0 0 #0000 !important;
|
|
845
|
+
--tw-ring-offset-shadow: 0 0 #0000 !important;
|
|
846
|
+
transition: border-color .15s ease;
|
|
847
|
+
}
|
|
848
|
+
#chat-input:focus,
|
|
849
|
+
#chat-input:focus-visible {
|
|
850
|
+
outline: none !important;
|
|
851
|
+
box-shadow: none !important;
|
|
852
|
+
border-color: color-mix(in oklch, var(--color-primary) 55%, transparent);
|
|
853
|
+
}
|
|
854
|
+
#chat-input.textarea-warning {
|
|
855
|
+
border-color: color-mix(in oklch, var(--color-warning) 65%, transparent);
|
|
856
|
+
background: color-mix(in oklch, var(--color-warning) 6%, var(--color-base-100));
|
|
857
|
+
}
|
|
858
|
+
#chat-input.textarea-warning:focus {
|
|
859
|
+
border-color: color-mix(in oklch, var(--color-warning) 80%, transparent);
|
|
860
|
+
}
|
|
861
|
+
|
|
750
862
|
/* ── Typing dots ─────────────────────────────────────────────────────────── */
|
|
751
863
|
.dot { width: 5px; height: 5px; background: currentColor; border-radius: 50%; display: inline-block; animation: blink 1.2s infinite; }
|
|
752
864
|
.dot:nth-child(2) { animation-delay: .4s; }
|
|
@@ -999,6 +1111,26 @@
|
|
|
999
1111
|
font-size: 10px;
|
|
1000
1112
|
opacity: 0.75;
|
|
1001
1113
|
}
|
|
1114
|
+
.ma-approval {
|
|
1115
|
+
font-size: 12px;
|
|
1116
|
+
background: color-mix(in oklch, var(--color-warning) 10%, var(--color-base-200));
|
|
1117
|
+
border: 1px solid color-mix(in oklch, var(--color-warning) 45%, var(--color-base-300));
|
|
1118
|
+
border-radius: 8px;
|
|
1119
|
+
padding: 8px 10px;
|
|
1120
|
+
margin-right: 8%;
|
|
1121
|
+
}
|
|
1122
|
+
.ma-approval-label { font-weight: 600; margin-bottom: 4px; }
|
|
1123
|
+
.ma-approval-cmd {
|
|
1124
|
+
font-family: ui-monospace, monospace;
|
|
1125
|
+
font-size: 11px;
|
|
1126
|
+
white-space: pre-wrap;
|
|
1127
|
+
word-break: break-all;
|
|
1128
|
+
background: var(--color-base-100);
|
|
1129
|
+
border-radius: 6px;
|
|
1130
|
+
padding: 6px 8px;
|
|
1131
|
+
margin: 4px 0;
|
|
1132
|
+
}
|
|
1133
|
+
.ma-approval-actions { display: flex; gap: 6px; justify-content: flex-end; }
|
|
1002
1134
|
.ma-thumb {
|
|
1003
1135
|
width: 56px;
|
|
1004
1136
|
height: 56px;
|