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,192 @@
1
+ // shell/hub.js — multi-machine picker, menu, polling
2
+ import { batch, esc, $ } from '../lib.js';
3
+ import {
4
+ machinesList, hubMode, hubMachineReady, selectedMachineId, setSelectedMachine,
5
+ currentProject, sessionFilter, viewingFile, ctx,
6
+ } from '../state.js';
7
+ import { api, probeHub } from '../api.js';
8
+ import { loadAllSessions, loadWorkspaces } from './session-list.js';
9
+ import { loadSessionMetaMap } from './notes.js';
10
+ // goHome imported dynamically in enterMachine to avoid circular deps
11
+
12
+ export async function refreshMachinesList() {
13
+ try {
14
+ const list = await api('GET', '/api/machines');
15
+ machinesList.value = Array.isArray(list) ? list : [];
16
+ } catch {
17
+ try {
18
+ const hub = await probeHub();
19
+ if (hub?.machines) machinesList.value = hub.machines;
20
+ } catch { /* ignore */ }
21
+ }
22
+ return machinesList.peek() || [];
23
+ }
24
+
25
+ export function renderMachinePickerList() {
26
+ const listEl = $('machine-picker-list');
27
+ const emptyEl = $('machine-picker-empty');
28
+ if (!listEl) return;
29
+ const ms = machinesList.peek() || [];
30
+ if (!ms.length) {
31
+ listEl.innerHTML = '';
32
+ emptyEl?.classList.remove('hidden');
33
+ return;
34
+ }
35
+ emptyEl?.classList.add('hidden');
36
+ listEl.innerHTML = ms.map(m => {
37
+ const online = m.online !== false;
38
+ const meta = [m.hostname, m.platform].filter(Boolean).join(' · ');
39
+ return `
40
+ <button type="button" class="machine-pick-card w-full text-left px-4 py-3 rounded-xl border border-base-300
41
+ hover:border-primary hover:bg-primary/5 transition-colors ${online ? '' : 'opacity-50'}"
42
+ data-pick-machine="${esc(m.id)}" ${online ? '' : 'disabled'}>
43
+ <div class="flex items-center gap-2">
44
+ <span class="w-2 h-2 rounded-full flex-shrink-0 ${online ? 'bg-success' : 'bg-error'}"></span>
45
+ <span class="font-semibold text-sm font-mono">${esc(m.id)}</span>
46
+ </div>
47
+ ${meta ? `<div class="text-xs text-base-content/45 mt-1 pl-4">${esc(meta)}</div>` : ''}
48
+ </button>`;
49
+ }).join('');
50
+ }
51
+
52
+ export function showMachinePicker() {
53
+ const el = $('machine-picker');
54
+ if (!el) return;
55
+ el.classList.remove('hidden');
56
+ hubMachineReady.value = false;
57
+ renderMachinePickerList();
58
+ const status = $('machine-picker-status');
59
+ if (status) {
60
+ const n = (machinesList.peek() || []).filter(m => m.online !== false).length;
61
+ status.textContent = `${n} online`;
62
+ }
63
+ }
64
+
65
+ export function hideMachinePicker() {
66
+ $('machine-picker')?.classList.add('hidden');
67
+ hubMachineReady.value = true;
68
+ }
69
+
70
+ export function closeMachineMenu() {
71
+ $('machine-menu')?.classList.add('hidden');
72
+ }
73
+
74
+ export function renderMachineMenuList() {
75
+ const listEl = $('machine-menu-list');
76
+ const emptyEl = $('machine-menu-empty');
77
+ if (!listEl) return;
78
+ const ms = machinesList.peek() || [];
79
+ const cur = selectedMachineId.peek() || '';
80
+ if (!ms.length) {
81
+ listEl.innerHTML = '';
82
+ emptyEl?.classList.remove('hidden');
83
+ return;
84
+ }
85
+ emptyEl?.classList.add('hidden');
86
+ listEl.innerHTML = ms.map(m => {
87
+ const online = m.online !== false;
88
+ const active = m.id === cur;
89
+ const meta = [m.hostname, m.platform].filter(Boolean).join(' · ');
90
+ const since = m.connectedAt
91
+ ? new Date(m.connectedAt).toLocaleTimeString()
92
+ : '';
93
+ return `
94
+ <button type="button"
95
+ class="machine-menu-item w-full text-left px-2 py-1.5 rounded-md text-xs
96
+ ${active ? 'bg-primary/15 text-primary' : 'hover:bg-base-200'}
97
+ ${online ? '' : 'opacity-50'}"
98
+ data-pick-machine="${esc(m.id)}"
99
+ ${online ? '' : 'disabled'}>
100
+ <div class="flex items-center gap-1.5">
101
+ <span class="w-1.5 h-1.5 rounded-full flex-shrink-0 ${online ? 'bg-success' : 'bg-error'}"></span>
102
+ <span class="font-mono font-semibold truncate flex-1">${esc(m.id)}</span>
103
+ ${active ? '<span class="text-[9px] opacity-70">当前</span>' : ''}
104
+ <span class="text-[9px] ${online ? 'text-success' : 'text-error'}">${online ? 'online' : 'offline'}</span>
105
+ </div>
106
+ ${meta || since ? `<div class="text-[10px] text-base-content/40 pl-3.5 mt-0.5 truncate">${esc(meta)}${since ? ' · ' + esc(since) : ''}</div>` : ''}
107
+ </button>`;
108
+ }).join('');
109
+ }
110
+
111
+ export function syncTopbarMachine() {
112
+ const wrap = $('machine-menu-wrap');
113
+ const label = $('topbar-machine-label');
114
+ const dot = $('topbar-machine-dot');
115
+ if (!hubMode.peek()) {
116
+ wrap?.classList.add('hidden');
117
+ return;
118
+ }
119
+ wrap?.classList.remove('hidden');
120
+ const cur = selectedMachineId.peek() || '';
121
+ const ms = machinesList.peek() || [];
122
+ const info = ms.find(m => m.id === cur);
123
+ const online = info ? info.online !== false : false;
124
+ if (label) label.textContent = cur || '选择机器';
125
+ if (dot) {
126
+ dot.classList.toggle('bg-success', !!cur && online);
127
+ dot.classList.toggle('bg-warning', !!cur && !online);
128
+ dot.classList.toggle('bg-base-content/30', !cur);
129
+ }
130
+ renderMachineMenuList();
131
+ }
132
+
133
+ let _machinePollTimer = null;
134
+ export function startMachinePolling() {
135
+ if (_machinePollTimer) return;
136
+ _machinePollTimer = setInterval(async () => {
137
+ if (!hubMode.peek() || !ctx.token) return;
138
+ await refreshMachinesList();
139
+ syncTopbarMachine();
140
+ // If current machine went offline, warn in topbar label only (don't force picker)
141
+ const cur = selectedMachineId.peek();
142
+ if (cur) {
143
+ const m = (machinesList.peek() || []).find(x => x.id === cur);
144
+ if (m && m.online === false) {
145
+ const label = $('topbar-machine-label');
146
+ if (label) label.textContent = `${cur} (offline)`;
147
+ }
148
+ }
149
+ }, 15000);
150
+ }
151
+
152
+ /** Enter / switch hub machine. */
153
+ export async function enterMachine(machineId, { forceReload = false } = {}) {
154
+ if (!machineId) return;
155
+ const prev = selectedMachineId.peek();
156
+ const switching = prev && prev !== machineId;
157
+ setSelectedMachine(machineId);
158
+ hideMachinePicker();
159
+ closeMachineMenu();
160
+ syncTopbarMachine();
161
+
162
+ if (switching || forceReload) {
163
+ try { ctx.ws?.close(); } catch {}
164
+ ctx.ws = null;
165
+ ctx.sessionId = null;
166
+ batch(() => {
167
+ currentProject.value = null;
168
+ sessionFilter.value = null;
169
+ viewingFile.value = null;
170
+ });
171
+ // Drop previous machine paths so Files/Git don't browse the wrong host
172
+ try {
173
+ const { filesRoot, gitRoot, filesPath } = await import('../state.js');
174
+ filesRoot.value = '';
175
+ gitRoot.value = '';
176
+ filesPath.value = '';
177
+ } catch { /* ignore */ }
178
+ if (switching) {
179
+ const { goHome } = await import('./session-nav.js');
180
+ goHome();
181
+ }
182
+ }
183
+
184
+ const bar = $('topbar-project');
185
+ if (bar && (!currentProject.peek())) {
186
+ bar.textContent = `Machine · ${machineId}`;
187
+ }
188
+
189
+ await Promise.all([loadAllSessions(), loadWorkspaces(), loadSessionMetaMap()]);
190
+ hubMachineReady.value = true;
191
+ }
192
+
@@ -0,0 +1,195 @@
1
+ // shell/layout.js — sidebar, theme, new-session modal
2
+ import { batch, esc, $ } from '../lib.js';
3
+ import {
4
+ workspacesData, sessionFilter, filesPath, viewingFile, currentProject,
5
+ filesRoot, gitRoot, currentModel, currentAgent, setAgent,
6
+ hubMode, machinesList, selectedMachineId, setSelectedMachine,
7
+ getDefaultModel, AGENT_LABELS, ctx,
8
+ } from '../state.js';
9
+ // filesRoot/gitRoot used when starting a new session (default paths)
10
+ import { api } from '../api.js';
11
+ import { connectWS, clearMessages, appendSystemMsg, applyChatDensity } from '../chat.js';
12
+ import { refreshModelSelect } from './session-list.js';
13
+ import { switchTab } from './session-nav.js';
14
+ import { showMachinePicker } from './hub.js';
15
+
16
+ // ── Sidebar ───────────────────────────────────────────────────────────────────
17
+ export function openSidebar() { $('sidebar').classList.add('open'); $('sidebar-overlay').classList.remove('hidden'); }
18
+ export function closeSidebar() { $('sidebar').classList.remove('open'); $('sidebar-overlay').classList.add('hidden'); }
19
+ export function closeSidebarOnMobile() { if (window.innerWidth <= 640) closeSidebar(); }
20
+
21
+ export function initSidebarToggle() {
22
+ const btn = $('sidebar-toggle');
23
+ const sidebar = $('sidebar');
24
+ let collapsed = false;
25
+ let savedWidth = sidebar.style.width || '260px';
26
+
27
+ btn.addEventListener('click', () => {
28
+ collapsed = !collapsed;
29
+ if (collapsed) {
30
+ savedWidth = sidebar.style.width || '260px';
31
+ sidebar.style.width = '0';
32
+ sidebar.style.minWidth = '0';
33
+ sidebar.style.overflow = 'hidden';
34
+ } else {
35
+ sidebar.style.width = savedWidth;
36
+ sidebar.style.minWidth = '';
37
+ sidebar.style.overflow = '';
38
+ }
39
+ btn.textContent = collapsed ? '▶' : '◀';
40
+ });
41
+ }
42
+
43
+ export function initSidebarResize() {
44
+ const handle = $('sidebar-resize');
45
+ const sidebar = $('sidebar');
46
+ let dragging = false;
47
+
48
+ handle.addEventListener('mousedown', e => {
49
+ dragging = true;
50
+ document.body.style.userSelect = 'none';
51
+ document.body.style.cursor = 'col-resize';
52
+ e.preventDefault();
53
+ });
54
+ document.addEventListener('mousemove', e => {
55
+ if (!dragging) return;
56
+ const w = Math.min(Math.max(e.clientX, 160), 520);
57
+ sidebar.style.width = w + 'px';
58
+ sidebar.style.minWidth = '';
59
+ });
60
+ document.addEventListener('mouseup', () => {
61
+ if (!dragging) return;
62
+ dragging = false;
63
+ document.body.style.userSelect = '';
64
+ document.body.style.cursor = '';
65
+ });
66
+ }
67
+
68
+ // ── Theme ─────────────────────────────────────────────────────────────────────
69
+ export function applyTheme(theme) {
70
+ document.documentElement.setAttribute('data-theme', theme);
71
+ localStorage.setItem('theme', theme);
72
+ const btn = $('btn-theme');
73
+ if (btn) btn.textContent = theme === 'dark' || theme === 'night' || theme === 'black' ? '🌙' : '☀️';
74
+ // Swap highlight.js stylesheet for light/dark
75
+ let link = document.getElementById('hljs-theme');
76
+ if (!link) {
77
+ link = document.querySelector('link[href*="highlight"][href*="styles"]');
78
+ if (link) link.id = 'hljs-theme';
79
+ }
80
+ if (link) {
81
+ const dark = theme === 'dark' || theme === 'night' || theme === 'black';
82
+ link.href = dark
83
+ ? 'https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github-dark.min.css'
84
+ : 'https://cdn.jsdelivr.net/npm/@highlightjs/cdn-assets@11/styles/github.min.css';
85
+ }
86
+ }
87
+
88
+ export function toggleTheme() {
89
+ const cur = document.documentElement.getAttribute('data-theme') || 'dark';
90
+ applyTheme(cur === 'dark' ? 'light' : 'dark');
91
+ }
92
+
93
+ // ── New Session modal ─────────────────────────────────────────────────────────
94
+ export function openNewSessionModal() {
95
+ const agentSel = $('new-session-agent');
96
+ if (agentSel) agentSel.value = currentAgent.peek() || 'claude';
97
+ const sel = $('new-session-workspace');
98
+ if (sel) {
99
+ const wss = workspacesData.peek();
100
+ sel.innerHTML = wss.map(w =>
101
+ `<option value="${esc(w.configDir)}">${esc(w.name)} — ${esc(w.configDir)}</option>`
102
+ ).join('');
103
+ // pre-select current session's workspace
104
+ if (ctx.configDir) sel.value = ctx.configDir;
105
+ }
106
+ const mwrap = $('new-session-machine-wrap');
107
+ const msel = $('new-session-machine');
108
+ if (mwrap && msel) {
109
+ // Machine already chosen at login; still show locked selection for clarity
110
+ if (hubMode.peek()) {
111
+ mwrap.style.display = '';
112
+ const ms = machinesList.peek() || [];
113
+ const cur = selectedMachineId.peek() || '';
114
+ msel.innerHTML = ms.length
115
+ ? ms.map(m => `<option value="${esc(m.id)}">${esc(m.id)}${m.hostname ? ' — ' + esc(m.hostname) : ''}</option>`).join('')
116
+ : '<option value="">(no machines online)</option>';
117
+ if (cur) msel.value = cur;
118
+ msel.disabled = true;
119
+ } else {
120
+ mwrap.style.display = 'none';
121
+ if (msel) msel.disabled = false;
122
+ }
123
+ }
124
+ updateNewSessionAgentUI();
125
+ $('new-session-path').value = currentProject.peek()?.path || '';
126
+ $('new-session-error').classList.add('hidden');
127
+ $('modal-new-session').showModal();
128
+ setTimeout(() => $('new-session-path').focus(), 50);
129
+ }
130
+
131
+ export function updateNewSessionAgentUI() {
132
+ const agent = $('new-session-agent')?.value || 'claude';
133
+ const wrap = $('new-session-ws-wrap');
134
+ if (wrap) wrap.style.display = agent === 'claude' ? '' : 'none';
135
+ }
136
+
137
+ export async function startNewSession() {
138
+ const rawPath = $('new-session-path').value.trim();
139
+ const agent = $('new-session-agent')?.value || 'claude';
140
+ const configDir = agent === 'claude' ? $('new-session-workspace').value : null;
141
+ const errEl = $('new-session-error');
142
+ errEl.classList.add('hidden');
143
+
144
+ if (!rawPath) { errEl.textContent = 'Working directory is required'; errEl.classList.remove('hidden'); return; }
145
+
146
+ // Hub: new session uses currently selected machine
147
+ if (hubMode.peek()) {
148
+ const mid = selectedMachineId.peek() || $('new-session-machine')?.value || machinesList.peek()?.[0]?.id;
149
+ if (!mid) {
150
+ errEl.textContent = '请先选择一台机器';
151
+ errEl.classList.remove('hidden');
152
+ showMachinePicker();
153
+ return;
154
+ }
155
+ setSelectedMachine(mid);
156
+ }
157
+
158
+ try {
159
+ const result = await api('POST', '/api/resolve-path', { path: rawPath });
160
+ if (!result.isDir) throw new Error('Path is not a directory');
161
+
162
+ $('modal-new-session').close();
163
+
164
+ const name = result.path.split('/').filter(Boolean).pop() || result.path;
165
+ ctx.sessionId = null;
166
+ ctx.configDir = configDir || null;
167
+ ctx.agent = agent;
168
+ setAgent(agent);
169
+ currentModel.value = getDefaultModel(agent) || currentModel.peek();
170
+ localStorage.setItem('model', currentModel.peek());
171
+ refreshModelSelect();
172
+ batch(() => {
173
+ currentProject.value = { id: name, name, path: result.path, machineId: selectedMachineId.peek() || ctx.machineId };
174
+ sessionFilter.value = result.path;
175
+ filesRoot.value = result.path;
176
+ gitRoot.value = result.path;
177
+ filesPath.value = '';
178
+ viewingFile.value = null;
179
+ });
180
+ $('topbar-project').textContent = result.path;
181
+ const fi = $('files-root-input'); if (fi) fi.value = result.path;
182
+ const gi = $('git-root-input'); if (gi) gi.value = result.path;
183
+ $('welcome').classList.add('hidden');
184
+ const pv = $('project-view');
185
+ pv.classList.remove('hidden');
186
+ pv.style.display = 'flex';
187
+ clearMessages();
188
+ connectWS();
189
+ switchTab('chat');
190
+ appendSystemMsg(`New ${AGENT_LABELS[agent] || agent} session · ${result.path}`);
191
+ } catch (e) {
192
+ errEl.textContent = e.message;
193
+ errEl.classList.remove('hidden');
194
+ }
195
+ }