claude-simple 0.2.24 → 0.2.25
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 +20 -0
- package/client.js +20 -3
- package/dist/app.0.2.25.min.js +1164 -0
- package/dist/index.html +2 -2
- package/dist/style.0.2.25.min.css +1753 -0
- package/package.json +1 -1
- package/public/api.js +1 -0
- package/public/chat.js +213 -8
- package/public/router.js +5 -0
- package/public/settings.js +312 -63
- package/public/shell/notes.js +5 -2
- package/public/shell/session-list.js +50 -2
- package/public/shell/session-nav.js +45 -12
- package/public/state.js +3 -2
- package/public/style.css +126 -0
- package/public/ui.min.css +1 -1
- package/src/agent-env.js +211 -0
- package/src/agents/claude.js +10 -3
- package/src/agents/grok.js +24 -0
- package/src/agents/models.js +30 -1
- package/src/agents/sessions.js +12 -2
- package/src/app.js +450 -14
- package/src/collab.js +248 -0
- package/src/runtime/external-session-sync.js +106 -0
- package/src/runtime/protocol.js +3 -2
- package/dist/app.0.2.24.min.js +0 -1104
- package/dist/style.0.2.24.min.css +0 -1627
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { batch, esc, $ } from '../lib.js';
|
|
3
3
|
import {
|
|
4
4
|
sessionsData, sessionFilter, filesPath, viewingFile, filteredSessions,
|
|
5
|
-
currentProject, currentTab, filesRoot, gitRoot, currentAgent, setAgent,
|
|
5
|
+
currentProject, currentTab, filesRoot, gitRoot, currentAgent, setAgent, isProcessing,
|
|
6
6
|
selectedMachineId, setSelectedMachine, AGENT_LABELS, ctx, hubMode,
|
|
7
7
|
} from '../state.js';
|
|
8
8
|
import { api } from '../api.js';
|
|
@@ -10,13 +10,31 @@ 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
|
+
stashComposerDraft, restoreComposerDraft, discardComposerAttachments, parkQueuedMessages, detachProcessingForSessionSwitch, setExternalSessionReadOnly,
|
|
14
14
|
} from '../chat.js';
|
|
15
15
|
import { setHash, syncHash } from '../router.js';
|
|
16
16
|
import { refreshModelSelect } from './session-list.js';
|
|
17
17
|
import { renderSessionNotesBar } from './notes.js';
|
|
18
18
|
import { addRecentSession, updateDashboard, getDashboardSessions } from './dashboard.js';
|
|
19
19
|
|
|
20
|
+
// A terminal-owned agent writes its native transcript independently of this
|
|
21
|
+
// UI. Keep just the currently open session hot and coalesce file updates into
|
|
22
|
+
// one fresh history read, rather than attempting to replay every vendor JSONL
|
|
23
|
+
// event in the browser.
|
|
24
|
+
let activeExternalHistory = null;
|
|
25
|
+
let externalHistoryTimer = null;
|
|
26
|
+
|
|
27
|
+
document.addEventListener('external-session-updated', ({ detail }) => {
|
|
28
|
+
const active = activeExternalHistory;
|
|
29
|
+
if (!active || !detail || detail.sessionId !== active.sessionId) return;
|
|
30
|
+
if (detail.agent && active.agent && detail.agent !== active.agent) return;
|
|
31
|
+
if (externalHistoryTimer) clearTimeout(externalHistoryTimer);
|
|
32
|
+
externalHistoryTimer = setTimeout(() => {
|
|
33
|
+
externalHistoryTimer = null;
|
|
34
|
+
activeExternalHistory?.refresh();
|
|
35
|
+
}, 140);
|
|
36
|
+
});
|
|
37
|
+
|
|
20
38
|
/** Show which edge machine Files/Git/Shell paths belong to. */
|
|
21
39
|
function syncMachinePathHints(machineId) {
|
|
22
40
|
for (const id of ['files-machine-hint', 'git-machine-hint']) {
|
|
@@ -35,6 +53,8 @@ function syncMachinePathHints(machineId) {
|
|
|
35
53
|
}
|
|
36
54
|
|
|
37
55
|
export function openProject(project) {
|
|
56
|
+
activeExternalHistory = null;
|
|
57
|
+
setExternalSessionReadOnly(false);
|
|
38
58
|
stashComposerDraft();
|
|
39
59
|
parkQueuedMessages();
|
|
40
60
|
discardComposerAttachments();
|
|
@@ -67,6 +87,8 @@ export function openProject(project) {
|
|
|
67
87
|
}
|
|
68
88
|
|
|
69
89
|
export function goHome() {
|
|
90
|
+
activeExternalHistory = null;
|
|
91
|
+
setExternalSessionReadOnly(false);
|
|
70
92
|
stashComposerDraft();
|
|
71
93
|
parkQueuedMessages();
|
|
72
94
|
discardComposerAttachments();
|
|
@@ -144,6 +166,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
144
166
|
}
|
|
145
167
|
|
|
146
168
|
ctx.sessionId = sid;
|
|
169
|
+
setExternalSessionReadOnly(false);
|
|
147
170
|
ctx.configDir = resolvedConfig;
|
|
148
171
|
ctx.agent = resolvedAgent;
|
|
149
172
|
setAgent(resolvedAgent);
|
|
@@ -153,7 +176,6 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
153
176
|
// Default path = session cwd on the target machine (set immediately; no await)
|
|
154
177
|
let edgeCwd = resolvedCwd;
|
|
155
178
|
applyProjectPaths(edgeCwd, resolvedMachine, sid);
|
|
156
|
-
connectWS();
|
|
157
179
|
|
|
158
180
|
// Background: normalize path on the edge (does not block chat)
|
|
159
181
|
if (edgeCwd) {
|
|
@@ -169,6 +191,9 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
169
191
|
|
|
170
192
|
// Switch to target tab BEFORE history (files/git/shell usable immediately)
|
|
171
193
|
clearMessages();
|
|
194
|
+
// Subscribe after clearing the prior transcript. A queued Grok snapshot can
|
|
195
|
+
// arrive immediately on socket-open and must belong to the new history.
|
|
196
|
+
connectWS();
|
|
172
197
|
switchTab(targetTab);
|
|
173
198
|
{
|
|
174
199
|
const cur = sessionsData.peek();
|
|
@@ -227,7 +252,7 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
227
252
|
? m.content.map(p => p?.text || '').filter(Boolean).join('\n')
|
|
228
253
|
: String(m.content ?? ''));
|
|
229
254
|
if (content) {
|
|
230
|
-
appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts });
|
|
255
|
+
appendMsg(m.role === 'user' ? 'user' : 'assistant', m.role === 'user' ? 'You' : label, content, { ts, historyIndex: i });
|
|
231
256
|
}
|
|
232
257
|
} else if (typ === 'tool_use' || m.name) {
|
|
233
258
|
renderToolUse({ name: m.name || 'tool', input: m.input ?? m.arguments ?? {} }, { ts });
|
|
@@ -252,14 +277,13 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
252
277
|
paintChunk();
|
|
253
278
|
};
|
|
254
279
|
|
|
255
|
-
const loadHistory = async () => {
|
|
280
|
+
const loadHistory = async ({ preferCache = true } = {}) => {
|
|
256
281
|
// Instant re-open from in-memory cache (never trust empty cache — race/stale)
|
|
257
282
|
const cached = historyCache.get(cacheKey);
|
|
258
|
-
if (cached && cached.msgs?.length && Date.now() - cached.at < HISTORY_CACHE_TTL) {
|
|
283
|
+
if (preferCache && cached && cached.msgs?.length && Date.now() - cached.at < HISTORY_CACHE_TTL) {
|
|
259
284
|
paintMessages(cached.msgs, cached.context, cached.meta);
|
|
260
|
-
return;
|
|
261
285
|
}
|
|
262
|
-
appendSystemMsg(`Loading ${label} history…`);
|
|
286
|
+
if (!cached?.msgs?.length) appendSystemMsg(`Loading ${label} history…`);
|
|
263
287
|
try {
|
|
264
288
|
// Hub mode must route to the edge that owns the session.
|
|
265
289
|
const mid = resolvedMachine || selectedMachineId.peek() || ctx.machineId || null;
|
|
@@ -273,10 +297,6 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
273
297
|
const q = new URLSearchParams({
|
|
274
298
|
tail: '0',
|
|
275
299
|
compact: '1',
|
|
276
|
-
// Hub keeps a short-lived compact copy after completed turns. This
|
|
277
|
-
// makes a mobile re-open cache-first while the live WS remains the
|
|
278
|
-
// source for an in-progress answer.
|
|
279
|
-
cache: 'prefer',
|
|
280
300
|
});
|
|
281
301
|
if (agent) q.set('agent', agent);
|
|
282
302
|
// Also put machine in query for proxies that drop custom headers.
|
|
@@ -284,6 +304,9 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
284
304
|
return api('GET', `/api/sessions/${sid}/messages?${q}`, undefined, undefined, {
|
|
285
305
|
machineId: mid || undefined,
|
|
286
306
|
dedupe: false,
|
|
307
|
+
// A transcript can be written shortly after the final stream frame.
|
|
308
|
+
// Never let an HTTP/browser cache hide a newer user prompt.
|
|
309
|
+
cache: 'no-store',
|
|
287
310
|
});
|
|
288
311
|
};
|
|
289
312
|
|
|
@@ -299,6 +322,10 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
299
322
|
const meta = Array.isArray(data) ? {} : { truncated: data?.truncated, total: data?.total };
|
|
300
323
|
if (msgs.length) historyCache.set(cacheKey, { msgs, context, meta, at: Date.now() });
|
|
301
324
|
else historyCache.delete(cacheKey);
|
|
325
|
+
// A reconnect can receive new stream frames while this verification read
|
|
326
|
+
// is in flight. Keep those live frames authoritative; the fresh response
|
|
327
|
+
// is still retained for the next open.
|
|
328
|
+
if (isProcessing.peek() || $('messages')?.querySelector('.live-stream')) return;
|
|
302
329
|
paintMessages(msgs, context, meta);
|
|
303
330
|
} catch (e) {
|
|
304
331
|
if (ctx.sessionId !== sid) return;
|
|
@@ -308,6 +335,12 @@ export async function resumeSession(sid, cwd, configDir, agent, machineId, opts
|
|
|
308
335
|
}
|
|
309
336
|
};
|
|
310
337
|
|
|
338
|
+
activeExternalHistory = {
|
|
339
|
+
sessionId: sid,
|
|
340
|
+
agent: resolvedAgent,
|
|
341
|
+
refresh: () => loadHistory({ preferCache: false }).catch(() => {}),
|
|
342
|
+
};
|
|
343
|
+
|
|
311
344
|
// Never block tab UI on history; chat still loads ASAP in background
|
|
312
345
|
if (skipHistory) {
|
|
313
346
|
loadHistory().catch(() => {});
|
package/public/state.js
CHANGED
|
@@ -77,7 +77,7 @@ export const agentFilters = signal((() => {
|
|
|
77
77
|
export const timeRange = signal(localStorage.getItem('timeRange') || '14'); // '0'|'1'|'7'|'14'|'30'
|
|
78
78
|
export const activityHits = signal(null); // null | { q, results, loading, error }
|
|
79
79
|
export const activityLoading = signal(false);
|
|
80
|
-
/** agent:sessionId → { favorite, notes, title, hidden, updatedAt } (title = rename map) */
|
|
80
|
+
/** agent:sessionId → { favorite, notes, title, hidden, branch, updatedAt } (title = rename map) */
|
|
81
81
|
export const sessionMetaMap = signal({});
|
|
82
82
|
/** Sidebar: show only favorited sessions */
|
|
83
83
|
export const favoritesOnly = signal(localStorage.getItem('favoritesOnly') === '1');
|
|
@@ -107,6 +107,7 @@ export const ctx = {
|
|
|
107
107
|
ws: null,
|
|
108
108
|
shellBubble: null,
|
|
109
109
|
configDir: null,
|
|
110
|
+
externalSessionReadOnly: false,
|
|
110
111
|
};
|
|
111
112
|
|
|
112
113
|
/** Select edge machine (hub). Keeps signal + ctx + localStorage in sync. */
|
|
@@ -129,7 +130,7 @@ export function metaKey(agent, sessionId, machineId) {
|
|
|
129
130
|
|
|
130
131
|
export function getSessionMeta(agent, sessionId, machineId) {
|
|
131
132
|
const m = sessionMetaMap.peek()?.[metaKey(agent, sessionId, machineId)];
|
|
132
|
-
return m || { favorite: false, notes: '', title: '', hidden: false };
|
|
133
|
+
return m || { favorite: false, notes: '', title: '', hidden: false, branch: null };
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/** Display title: user rename map → original display → short id. */
|
package/public/style.css
CHANGED
|
@@ -733,6 +733,37 @@ html.pwa-standalone .ma-panel {
|
|
|
733
733
|
.session-row.session-thin {
|
|
734
734
|
opacity: 0.88;
|
|
735
735
|
}
|
|
736
|
+
.session-row.session-branch {
|
|
737
|
+
margin-left: calc(var(--branch-depth, 1) * 0.7rem);
|
|
738
|
+
border-left-style: dashed;
|
|
739
|
+
}
|
|
740
|
+
.session-branch-line {
|
|
741
|
+
margin-top: 1px;
|
|
742
|
+
font-size: 9px;
|
|
743
|
+
line-height: 1.25;
|
|
744
|
+
font-family: ui-monospace, Menlo, monospace;
|
|
745
|
+
color: color-mix(in oklch, var(--color-primary) 58%, var(--color-base-content));
|
|
746
|
+
overflow: hidden;
|
|
747
|
+
text-overflow: ellipsis;
|
|
748
|
+
white-space: nowrap;
|
|
749
|
+
}
|
|
750
|
+
.session-branch-badge {
|
|
751
|
+
font-size: 9px;
|
|
752
|
+
color: color-mix(in oklch, var(--color-primary) 82%, var(--color-base-content));
|
|
753
|
+
border: 1px solid color-mix(in oklch, var(--color-primary) 28%, transparent);
|
|
754
|
+
border-radius: 3px;
|
|
755
|
+
padding: 0 3px;
|
|
756
|
+
}
|
|
757
|
+
.session-branch-current {
|
|
758
|
+
margin-left: auto;
|
|
759
|
+
max-width: 10rem;
|
|
760
|
+
overflow: hidden;
|
|
761
|
+
text-overflow: ellipsis;
|
|
762
|
+
white-space: nowrap;
|
|
763
|
+
font-family: ui-monospace, Menlo, monospace;
|
|
764
|
+
font-size: 10px;
|
|
765
|
+
color: color-mix(in oklch, var(--color-primary) 72%, var(--color-base-content));
|
|
766
|
+
}
|
|
736
767
|
.session-thin-badge {
|
|
737
768
|
font-size: 9px;
|
|
738
769
|
font-family: ui-monospace, Menlo, monospace;
|
|
@@ -1304,6 +1335,41 @@ html.pwa-standalone .ma-panel {
|
|
|
1304
1335
|
user-select: none;
|
|
1305
1336
|
}
|
|
1306
1337
|
.msg-row.user .msg-ts { text-align: right; }
|
|
1338
|
+
.msg-row.queued-user-msg > div:not(.msg-ts) {
|
|
1339
|
+
opacity: .76;
|
|
1340
|
+
border-style: dashed;
|
|
1341
|
+
}
|
|
1342
|
+
.msg-row.cancelled-queued-user-msg > div:not(.msg-ts) {
|
|
1343
|
+
opacity: .48;
|
|
1344
|
+
text-decoration: line-through;
|
|
1345
|
+
}
|
|
1346
|
+
.msg-actions {
|
|
1347
|
+
display: flex;
|
|
1348
|
+
align-items: center;
|
|
1349
|
+
gap: 0.25rem;
|
|
1350
|
+
padding: 0 0.45rem;
|
|
1351
|
+
opacity: 0;
|
|
1352
|
+
transition: opacity .12s ease;
|
|
1353
|
+
}
|
|
1354
|
+
.msg-row.assistant:hover .msg-actions,
|
|
1355
|
+
.msg-row.assistant:focus-within .msg-actions { opacity: 1; }
|
|
1356
|
+
.msg-action-btn {
|
|
1357
|
+
appearance: none;
|
|
1358
|
+
border: 0;
|
|
1359
|
+
background: transparent;
|
|
1360
|
+
color: color-mix(in oklch, var(--color-base-content) 48%, transparent);
|
|
1361
|
+
font-size: 10px;
|
|
1362
|
+
line-height: 1.4;
|
|
1363
|
+
padding: 1px 4px;
|
|
1364
|
+
border-radius: 4px;
|
|
1365
|
+
cursor: pointer;
|
|
1366
|
+
}
|
|
1367
|
+
.msg-action-btn:hover:not(:disabled) {
|
|
1368
|
+
color: var(--color-primary);
|
|
1369
|
+
background: color-mix(in oklch, var(--color-primary) 12%, transparent);
|
|
1370
|
+
}
|
|
1371
|
+
.msg-action-btn:disabled { cursor: wait; opacity: .55; }
|
|
1372
|
+
@media (hover: none) { .msg-actions { opacity: 1; } }
|
|
1307
1373
|
|
|
1308
1374
|
.token-bar {
|
|
1309
1375
|
display: flex;
|
|
@@ -1623,3 +1689,63 @@ html.pwa-standalone .ma-panel {
|
|
|
1623
1689
|
padding: 0.05em 0 0.15em 0.5em !important;
|
|
1624
1690
|
}
|
|
1625
1691
|
}
|
|
1692
|
+
|
|
1693
|
+
/* ── Settings: full-page overlay using the same DaisyUI tokens as #app ── */
|
|
1694
|
+
|
|
1695
|
+
html.settings-open #app { visibility: hidden; }
|
|
1696
|
+
.set-page {
|
|
1697
|
+
position: fixed;
|
|
1698
|
+
inset: 0;
|
|
1699
|
+
z-index: 50;
|
|
1700
|
+
background: hsl(var(--b1));
|
|
1701
|
+
color: hsl(var(--bc));
|
|
1702
|
+
overflow: auto;
|
|
1703
|
+
min-height: 100dvh;
|
|
1704
|
+
}
|
|
1705
|
+
.set-island {
|
|
1706
|
+
position: sticky;
|
|
1707
|
+
top: 0;
|
|
1708
|
+
z-index: 2;
|
|
1709
|
+
display: flex;
|
|
1710
|
+
align-items: center;
|
|
1711
|
+
gap: 0.5rem;
|
|
1712
|
+
height: 44px;
|
|
1713
|
+
padding: 0 0.75rem;
|
|
1714
|
+
background: hsl(var(--b2));
|
|
1715
|
+
border-bottom: 1px solid hsl(var(--b3));
|
|
1716
|
+
}
|
|
1717
|
+
.set-split {
|
|
1718
|
+
display: grid;
|
|
1719
|
+
grid-template-columns: minmax(140px, 200px) minmax(0, 1fr);
|
|
1720
|
+
gap: 0.75rem;
|
|
1721
|
+
max-width: 56rem;
|
|
1722
|
+
margin: 1rem auto 2.5rem;
|
|
1723
|
+
padding: 0 1rem 2rem;
|
|
1724
|
+
}
|
|
1725
|
+
.set-nav { display: flex; flex-direction: column; gap: 0.25rem; padding-top: 0.25rem; }
|
|
1726
|
+
.set-nav-item.is-on {
|
|
1727
|
+
background: hsl(var(--b3));
|
|
1728
|
+
color: hsl(var(--bc));
|
|
1729
|
+
}
|
|
1730
|
+
.set-main { min-width: 0; }
|
|
1731
|
+
.set-panel { padding: 0.25rem 0 1.5rem; }
|
|
1732
|
+
.set-fields { display: grid; gap: 0.5rem; }
|
|
1733
|
+
.set-list { display: flex; flex-direction: column; gap: 0.5rem; margin-bottom: 1rem; }
|
|
1734
|
+
.set-row {
|
|
1735
|
+
display: flex;
|
|
1736
|
+
align-items: center;
|
|
1737
|
+
gap: 0.75rem;
|
|
1738
|
+
padding: 0.65rem 0.75rem;
|
|
1739
|
+
border-radius: 0.5rem;
|
|
1740
|
+
background: hsl(var(--b2));
|
|
1741
|
+
border: 1px solid hsl(var(--b3));
|
|
1742
|
+
}
|
|
1743
|
+
.set-row-body { flex: 1; min-width: 0; }
|
|
1744
|
+
.set-add { display: grid; gap: 0.5rem; }
|
|
1745
|
+
.set-token { margin-top: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
|
|
1746
|
+
.set-token textarea { min-height: 7rem; resize: vertical; }
|
|
1747
|
+
@media (max-width: 767px) {
|
|
1748
|
+
.set-split { grid-template-columns: 1fr; }
|
|
1749
|
+
.set-nav { flex-direction: row; overflow-x: auto; gap: 0.25rem; }
|
|
1750
|
+
.set-nav-item { white-space: nowrap; }
|
|
1751
|
+
}
|