claude-simple 0.1.0

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.
Files changed (50) hide show
  1. package/README.md +225 -0
  2. package/client.js +139 -0
  3. package/dist/addon-fit.min.js +2 -0
  4. package/dist/app.0.1.0.min.js +825 -0
  5. package/dist/highlight.min.css +10 -0
  6. package/dist/highlight.min.js +1244 -0
  7. package/dist/index.html +16 -0
  8. package/dist/marked.min.js +6 -0
  9. package/dist/style.0.1.0.min.css +1050 -0
  10. package/dist/xterm.min.css +209 -0
  11. package/dist/xterm.min.js +2 -0
  12. package/gateway.js +349 -0
  13. package/package.json +41 -0
  14. package/public/addon-fit.min.js +2 -0
  15. package/public/agent-panel.js +1079 -0
  16. package/public/api.js +78 -0
  17. package/public/app.js +95 -0
  18. package/public/base.css +4 -0
  19. package/public/cache.js +27 -0
  20. package/public/chat.js +1087 -0
  21. package/public/files.js +476 -0
  22. package/public/git.js +130 -0
  23. package/public/index.html +61 -0
  24. package/public/lib.js +2 -0
  25. package/public/router.js +71 -0
  26. package/public/settings.js +90 -0
  27. package/public/shell/boot.js +159 -0
  28. package/public/shell/dashboard.js +269 -0
  29. package/public/shell/hub.js +192 -0
  30. package/public/shell/layout.js +195 -0
  31. package/public/shell/notes.js +323 -0
  32. package/public/shell/session-context.js +19 -0
  33. package/public/shell/session-list.js +442 -0
  34. package/public/shell/session-nav.js +299 -0
  35. package/public/shell/templates.js +342 -0
  36. package/public/shell/util.js +63 -0
  37. package/public/shell.js +589 -0
  38. package/public/state.js +362 -0
  39. package/public/style.css +1048 -0
  40. package/public/terminal.js +177 -0
  41. package/public/xterm.min.css +209 -0
  42. package/public/xterm.min.js +2 -0
  43. package/server.js +12 -0
  44. package/src/agents/claude.js +58 -0
  45. package/src/agents/codex.js +284 -0
  46. package/src/agents/grok.js +318 -0
  47. package/src/agents/meta-agent.js +1952 -0
  48. package/src/agents/models.js +339 -0
  49. package/src/agents/sessions.js +1508 -0
  50. package/src/app.js +1282 -0
@@ -0,0 +1,323 @@
1
+ // shell/notes.js — project goal/notes + session favorites/notes/title (rename)
2
+ import { esc, $ } from '../lib.js';
3
+ import {
4
+ currentProject, sessionMetaMap, metaKey, getSessionMeta,
5
+ currentAgent, ctx,
6
+ } from '../state.js';
7
+ import { api } from '../api.js';
8
+
9
+ export let activeProjectGoal = '';
10
+ export let activeProjectNotes = '';
11
+
12
+ export function setProjectNoteFields(goal, notes) {
13
+ activeProjectGoal = goal || '';
14
+ activeProjectNotes = notes || '';
15
+ }
16
+
17
+ export async function fetchProjectNotes(rootPath) {
18
+ const bar = $('project-notes-bar');
19
+ if (!bar) return;
20
+
21
+ try {
22
+ const res = await api('GET', `/api/projects/notes?root=${encodeURIComponent(rootPath)}`);
23
+ activeProjectGoal = res.goal || '';
24
+ activeProjectNotes = res.notes || '';
25
+ renderProjectNotesDisplayMode();
26
+ bar.style.display = 'flex';
27
+ } catch (err) {
28
+ console.error('Error fetching project notes:', err);
29
+ bar.style.display = 'none';
30
+ }
31
+ }
32
+
33
+ export function renderProjectNotesDisplayMode() {
34
+ const bar = $('project-notes-bar');
35
+ if (!bar) return;
36
+
37
+ const goalText = activeProjectGoal || 'No goal yet — click Edit';
38
+ const notesText = activeProjectNotes || '';
39
+ bar.innerHTML = `
40
+ <div class="flex items-start gap-2 w-full">
41
+ <div class="flex-1 min-w-0">
42
+ <div class="truncate">
43
+ <span class="font-bold text-base-content/40 uppercase mr-1">Goal:</span>
44
+ <span id="project-goal-text" class="text-base-content/80 cursor-pointer hover:underline" title="Click to edit">${esc(goalText)}</span>
45
+ </div>
46
+ ${notesText
47
+ ? `<div class="mt-0.5 text-base-content/55 line-clamp-2 whitespace-pre-wrap" id="project-notes-text" title="${esc(notesText)}"><span class="font-bold text-base-content/35 uppercase mr-1">Notes:</span>${esc(notesText)}</div>`
48
+ : `<div class="mt-0.5 text-base-content/35 italic text-[11px]" id="project-notes-text">No project notes</div>`}
49
+ </div>
50
+ <button id="btn-edit-project-notes" class="btn btn-ghost btn-xs px-1.5 hover:bg-base-300 flex-shrink-0">Edit</button>
51
+ </div>
52
+ `;
53
+ }
54
+
55
+ export function renderProjectNotesEditMode() {
56
+ const bar = $('project-notes-bar');
57
+ if (!bar) return;
58
+
59
+ bar.innerHTML = `
60
+ <div class="flex flex-col gap-1.5 w-full">
61
+ <div class="flex items-center gap-2">
62
+ <span class="text-[10px] font-bold text-base-content/40 uppercase w-12 flex-shrink-0">Goal</span>
63
+ <input id="project-goal-input" type="text" class="input input-xs input-bordered flex-1 text-xs"
64
+ placeholder="One-line project goal…" value="${esc(activeProjectGoal)}">
65
+ </div>
66
+ <div class="flex items-start gap-2">
67
+ <span class="text-[10px] font-bold text-base-content/40 uppercase w-12 flex-shrink-0 mt-1">Notes</span>
68
+ <textarea id="project-notes-input" rows="2" class="textarea textarea-xs textarea-bordered flex-1 text-xs leading-snug"
69
+ placeholder="Longer project notes (stack, links, status…)">${esc(activeProjectNotes)}</textarea>
70
+ </div>
71
+ <div class="flex items-center gap-1 justify-end">
72
+ <button id="btn-save-project-notes" class="btn btn-primary btn-xs px-2">Save</button>
73
+ <button id="btn-cancel-project-notes" class="btn btn-ghost btn-xs px-2">Cancel</button>
74
+ </div>
75
+ </div>
76
+ `;
77
+
78
+ $('project-goal-input')?.focus();
79
+ }
80
+
81
+ export async function loadSessionMetaMap() {
82
+ try {
83
+ const db = await api('GET', '/api/sessions/meta');
84
+ sessionMetaMap.value = db || {};
85
+ } catch (e) {
86
+ console.warn('session meta load failed', e);
87
+ }
88
+ }
89
+
90
+ export function renderSessionNotesBar() {
91
+ const bar = $('session-notes-bar');
92
+ if (!bar) return;
93
+ const sid = ctx.sessionId;
94
+ const agent = ctx.agent || currentAgent.peek() || 'claude';
95
+ if (!sid) {
96
+ bar.style.display = 'none';
97
+ bar.innerHTML = '';
98
+ return;
99
+ }
100
+ // Don't clobber in-progress inline rename on the bar
101
+ if (bar.querySelector('.session-rename-input')) return;
102
+
103
+ const meta = getSessionMeta(agent, sid);
104
+ const note = (meta.notes || '').trim();
105
+ const fav = !!meta.favorite;
106
+ const title = (meta.title || '').trim();
107
+ const showTitle = title || sid.slice(0, 8);
108
+ bar.style.display = 'flex';
109
+ bar.innerHTML = `
110
+ <div class="flex-1 min-w-0 flex items-center gap-2">
111
+ <button type="button" class="session-fav-btn ${fav ? 'is-fav' : ''}" data-fav-toggle="1"
112
+ data-fav-session-id="${esc(sid)}" data-fav-session-agent="${esc(agent)}"
113
+ title="${fav ? 'Unfavorite' : 'Favorite'}">${fav ? '★' : '☆'}</button>
114
+ <span id="session-title-display"
115
+ class="session-title-inline max-w-[40%] truncate font-semibold text-base-content/85 cursor-text hover:text-primary"
116
+ title="Click to rename"
117
+ data-rename-bar="1">${esc(showTitle)}</span>
118
+ <span class="font-bold text-base-content/40 uppercase flex-shrink-0">Note:</span>
119
+ <span class="truncate text-base-content/70 ${note ? '' : 'italic text-base-content/40'}"
120
+ id="session-notes-text" title="${esc(note || 'Add a note for this session')}">${esc(note || 'No session note')}</span>
121
+ </div>
122
+ <button id="btn-edit-session-notes" class="btn btn-ghost btn-xs px-1.5">Note</button>
123
+ `;
124
+ }
125
+
126
+ export function renderSessionNotesEditMode() {
127
+ const bar = $('session-notes-bar');
128
+ if (!bar || !ctx.sessionId) return;
129
+ const agent = ctx.agent || currentAgent.peek() || 'claude';
130
+ const meta = getSessionMeta(agent, ctx.sessionId);
131
+ bar.innerHTML = `
132
+ <input id="session-notes-input" type="text" class="input input-xs input-bordered flex-1 text-xs"
133
+ placeholder="Note for this session…" value="${esc(meta.notes || '')}">
134
+ <button id="btn-save-session-notes" class="btn btn-primary btn-xs px-2">Save</button>
135
+ <button id="btn-cancel-session-notes" class="btn btn-ghost btn-xs px-2">Cancel</button>
136
+ `;
137
+ const input = $('session-notes-input');
138
+ input?.focus();
139
+ input?.select();
140
+ }
141
+
142
+ export async function toggleSessionFavorite(sessionId, agent) {
143
+ const key = metaKey(agent, sessionId);
144
+ const prev = sessionMetaMap.peek()?.[key] || { favorite: false, notes: '', title: '' };
145
+ const nextFav = !prev.favorite;
146
+ // optimistic
147
+ sessionMetaMap.value = {
148
+ ...sessionMetaMap.peek(),
149
+ [key]: { ...prev, favorite: nextFav, updatedAt: Date.now() },
150
+ };
151
+ try {
152
+ await api('PUT', '/api/sessions/meta', { sessionId, agent, favorite: nextFav });
153
+ } catch (e) {
154
+ sessionMetaMap.value = { ...sessionMetaMap.peek(), [key]: prev };
155
+ alert(`Favorite failed: ${e.message}`);
156
+ }
157
+ if (ctx.sessionId === sessionId) renderSessionNotesBar();
158
+ }
159
+
160
+ export async function saveSessionNotes(sessionId, agent, notes) {
161
+ const key = metaKey(agent, sessionId);
162
+ const prev = sessionMetaMap.peek()?.[key] || { favorite: false, notes: '', title: '' };
163
+ try {
164
+ const res = await api('PUT', '/api/sessions/meta', { sessionId, agent, notes });
165
+ sessionMetaMap.value = {
166
+ ...sessionMetaMap.peek(),
167
+ [key]: {
168
+ favorite: res.favorite ?? prev.favorite,
169
+ notes: res.notes ?? notes,
170
+ title: res.title ?? prev.title ?? '',
171
+ updatedAt: Date.now(),
172
+ },
173
+ };
174
+ } catch (e) {
175
+ alert(`Save note failed: ${e.message}`);
176
+ return false;
177
+ }
178
+ return true;
179
+ }
180
+
181
+ /**
182
+ * Rename session via meta title map (does not rewrite agent transcript files).
183
+ * Empty title clears the mapping and restores original display.
184
+ */
185
+ export async function renameSession(sessionId, agent, title, { machineId } = {}) {
186
+ if (!sessionId) return false;
187
+ const key = metaKey(agent, sessionId, machineId);
188
+ const prev = sessionMetaMap.peek()?.[key] || { favorite: false, notes: '', title: '' };
189
+ const nextTitle = String(title || '').trim().slice(0, 200);
190
+ // optimistic
191
+ sessionMetaMap.value = {
192
+ ...sessionMetaMap.peek(),
193
+ [key]: { ...prev, title: nextTitle, updatedAt: Date.now() },
194
+ };
195
+ try {
196
+ const res = await api('PUT', '/api/sessions/meta', { sessionId, agent, title: nextTitle });
197
+ sessionMetaMap.value = {
198
+ ...sessionMetaMap.peek(),
199
+ [key]: {
200
+ favorite: res.favorite ?? prev.favorite,
201
+ notes: res.notes ?? prev.notes ?? '',
202
+ title: res.title !== undefined ? res.title : nextTitle,
203
+ updatedAt: Date.now(),
204
+ },
205
+ };
206
+ // If server dropped empty entry, keep local clear
207
+ if (!res.title && !res.favorite && !res.notes) {
208
+ const map = { ...sessionMetaMap.peek() };
209
+ if (!nextTitle && !prev.favorite && !(prev.notes || '').trim()) {
210
+ delete map[key];
211
+ sessionMetaMap.value = map;
212
+ }
213
+ }
214
+ } catch (e) {
215
+ sessionMetaMap.value = { ...sessionMetaMap.peek(), [key]: prev };
216
+ alert(`Rename failed: ${e.message}`);
217
+ return false;
218
+ }
219
+ if (ctx.sessionId === sessionId) renderSessionNotesBar();
220
+ return true;
221
+ }
222
+
223
+ /**
224
+ * Inline rename — replace title in-place with an input (no dialog).
225
+ * Enter / blur = save; Escape = cancel. Empty value clears custom name.
226
+ *
227
+ * @param {{ sessionId: string, agent?: string, machineId?: string|null, fallbackDisplay?: string, mountEl: HTMLElement }} opts
228
+ */
229
+ export function startInlineRename({
230
+ sessionId,
231
+ agent = 'claude',
232
+ machineId = null,
233
+ fallbackDisplay = '',
234
+ mountEl,
235
+ } = {}) {
236
+ if (!sessionId || !mountEl) return;
237
+ // One editor at a time
238
+ document.querySelectorAll('.session-rename-input').forEach(el => {
239
+ el.dispatchEvent(new CustomEvent('session-rename-cancel', { bubbles: true }));
240
+ });
241
+
242
+ const meta = getSessionMeta(agent, sessionId, machineId);
243
+ const current = (meta.title || '').trim() || fallbackDisplay || sessionId.slice(0, 8);
244
+ const prevHtml = mountEl.innerHTML;
245
+ const prevClass = mountEl.className;
246
+
247
+ mountEl.classList.add('session-rename-active');
248
+ mountEl.innerHTML = `<input type="text" class="session-rename-input input input-xs input-bordered w-full min-w-0 text-xs font-mono"
249
+ value="${esc(current)}"
250
+ placeholder="Session name (empty = original)"
251
+ maxlength="200"
252
+ data-rename-input="1"
253
+ data-rename-session-id="${esc(sessionId)}"
254
+ data-rename-session-agent="${esc(agent)}"
255
+ data-rename-session-machine="${esc(machineId || '')}"
256
+ autocomplete="off" spellcheck="false" />`;
257
+
258
+ const input = mountEl.querySelector('.session-rename-input');
259
+ if (!input) return;
260
+
261
+ let done = false;
262
+ const finish = async (save) => {
263
+ if (done) return;
264
+ done = true;
265
+ const value = save ? input.value : current;
266
+ // Drop editor so list/bar effects can repaint
267
+ mountEl.className = prevClass;
268
+ if (!save) {
269
+ mountEl.innerHTML = prevHtml;
270
+ return;
271
+ }
272
+ await renameSession(sessionId, agent, value, { machineId });
273
+ // Clear editors so list/bar re-render is not blocked
274
+ document.querySelectorAll('.session-rename-input').forEach(n => n.remove());
275
+ if (ctx.sessionId === sessionId) renderSessionNotesBar();
276
+ sessionMetaMap.value = { ...sessionMetaMap.peek() };
277
+ };
278
+
279
+ input.addEventListener('keydown', (e) => {
280
+ e.stopPropagation();
281
+ if (e.key === 'Enter') {
282
+ e.preventDefault();
283
+ finish(true);
284
+ } else if (e.key === 'Escape') {
285
+ e.preventDefault();
286
+ finish(false);
287
+ }
288
+ });
289
+ input.addEventListener('click', (e) => e.stopPropagation());
290
+ input.addEventListener('mousedown', (e) => e.stopPropagation());
291
+ input.addEventListener('session-rename-cancel', () => finish(false));
292
+ // blur after a tick so click on another control still works
293
+ input.addEventListener('blur', () => {
294
+ setTimeout(() => finish(true), 0);
295
+ });
296
+
297
+ requestAnimationFrame(() => {
298
+ input.focus();
299
+ input.select();
300
+ });
301
+ }
302
+
303
+ /** @deprecated use startInlineRename — kept name for call sites */
304
+ export function promptRenameSession(sessionId, agent, opts = {}) {
305
+ // Prefer explicit mount; otherwise try bar / active row
306
+ let mountEl = opts.mountEl;
307
+ if (!mountEl && ctx.sessionId === sessionId) {
308
+ mountEl = document.getElementById('session-title-display');
309
+ }
310
+ if (!mountEl) {
311
+ mountEl = document.querySelector(
312
+ `.session-title-wrap[data-session-id="${CSS.escape(sessionId)}"]`,
313
+ );
314
+ }
315
+ if (!mountEl) return;
316
+ startInlineRename({
317
+ sessionId,
318
+ agent,
319
+ machineId: opts.machineId,
320
+ fallbackDisplay: opts.fallbackDisplay,
321
+ mountEl,
322
+ });
323
+ }
@@ -0,0 +1,19 @@
1
+ // shell/session-context.js — last-opened session metadata for fast deep-link restore
2
+ const LAST_SESSION_KEY = 'lastSessionContext';
3
+
4
+ export function getLastSessionContext() {
5
+ try {
6
+ const raw = localStorage.getItem(LAST_SESSION_KEY);
7
+ return raw ? JSON.parse(raw) : null;
8
+ } catch {
9
+ return null;
10
+ }
11
+ }
12
+
13
+ export function setLastSessionContext(partial) {
14
+ try {
15
+ const prev = getLastSessionContext() || {};
16
+ const next = { ...prev, ...partial, updatedAt: Date.now() };
17
+ localStorage.setItem(LAST_SESSION_KEY, JSON.stringify(next));
18
+ } catch { /* ignore quota / private mode */ }
19
+ }