claude-simple 0.2.23 → 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 +77 -8
- package/src/agents/models.js +30 -1
- package/src/agents/sessions.js +27 -10
- 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.23.min.js +0 -1104
- package/dist/style.0.2.23.min.css +0 -1627
package/public/settings.js
CHANGED
|
@@ -1,93 +1,235 @@
|
|
|
1
|
-
// settings.js —
|
|
1
|
+
// settings.js — full-page settings (not a crowded modal)
|
|
2
2
|
import { esc, $ } from './lib.js';
|
|
3
3
|
import { api } from './api.js';
|
|
4
4
|
import { workspacesData, hubMode, selectedMachineId } from './state.js';
|
|
5
5
|
import { setCachedWorkspaces } from './cache.js';
|
|
6
6
|
|
|
7
|
+
|
|
8
|
+
const CLAUDE_ENV_FIELDS = [
|
|
9
|
+
'ANTHROPIC_BASE_URL', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_MODEL',
|
|
10
|
+
'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL',
|
|
11
|
+
'ANTHROPIC_DEFAULT_HAIKU_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL',
|
|
12
|
+
'CLAUDE_CODE_EFFORT_LEVEL',
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
let activeSection = 'claude';
|
|
16
|
+
|
|
7
17
|
export function initSettings() {
|
|
8
|
-
document.getElementById('root').insertAdjacentHTML('beforeend',
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
18
|
+
document.getElementById('root').insertAdjacentHTML('beforeend', settingsMarkup());
|
|
19
|
+
bind();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function settingsMarkup() {
|
|
23
|
+
const fields = CLAUDE_ENV_FIELDS.map(key =>
|
|
24
|
+
`<label class="form-control"><div class="label py-1"><span class="label-text text-xs font-mono">${esc(key)}</span></div><input id="ae-${key}" class="input input-bordered input-sm font-mono text-xs" autocomplete="off" spellcheck="false"></label>`
|
|
25
|
+
).join('');
|
|
26
|
+
return `
|
|
27
|
+
<div id="settings-page" class="set-page" hidden>
|
|
28
|
+
<header class="set-island">
|
|
29
|
+
<button type="button" id="set-back" class="btn btn-ghost btn-sm px-2" aria-label="返回">←</button>
|
|
30
|
+
<span class="font-semibold text-sm">Settings</span>
|
|
31
|
+
</header>
|
|
32
|
+
<div class="set-split">
|
|
33
|
+
<nav class="set-nav" aria-label="设置分区">
|
|
34
|
+
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start is-on" data-set-sec="claude">Claude</button>
|
|
35
|
+
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="workspaces">Workspaces</button>
|
|
36
|
+
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="users" id="set-nav-users">Users</button>
|
|
37
|
+
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="password">Password</button>
|
|
38
|
+
<button type="button" class="set-nav-item btn btn-ghost btn-sm justify-start" data-set-sec="mcp">MCP</button>
|
|
39
|
+
</nav>
|
|
40
|
+
<div class="set-main">
|
|
41
|
+
<section class="set-panel" data-set-panel="claude">
|
|
42
|
+
<h2 class="text-lg font-semibold">Claude env</h2>
|
|
43
|
+
<p class="text-sm text-base-content/60 mb-4">Workspace 的 <b>name</b> 对应一套 ANTHROPIC_*。Default 是底,同名覆盖。</p>
|
|
44
|
+
<label class="form-control mb-3">
|
|
45
|
+
<div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Profile</span></div>
|
|
46
|
+
<select id="ae-profile" class="select select-bordered select-sm"></select>
|
|
47
|
+
</label>
|
|
48
|
+
<div class="set-fields">${fields}</div>
|
|
49
|
+
<p id="claude-env-error" class="text-error text-xs mt-2 hidden"></p>
|
|
50
|
+
<button type="button" id="claude-env-save" class="btn btn-primary btn-sm mt-4">Save profile</button>
|
|
51
|
+
</section>
|
|
52
|
+
<section class="set-panel" data-set-panel="workspaces" hidden>
|
|
53
|
+
<h2 class="text-lg font-semibold">Workspaces</h2>
|
|
54
|
+
<p class="text-sm text-base-content/60 mb-4">每个 name 一份 Claude 配置目录。加完后会立刻出现在上面的 Profile 列表里。</p>
|
|
55
|
+
<div id="ws-list" class="set-list"></div>
|
|
56
|
+
<div class="set-add">
|
|
57
|
+
<input id="ws-new-name" class="input input-bordered input-sm" placeholder="name e.g. deepseek">
|
|
58
|
+
<input id="ws-new-dir" class="input input-bordered input-sm font-mono text-xs" placeholder="~/.claude-deepseek">
|
|
59
|
+
<button type="button" id="ws-add-btn" class="btn btn-primary btn-sm">Add</button>
|
|
60
|
+
</div>
|
|
61
|
+
<p id="ws-error" class="text-error text-xs mt-2 hidden"></p>
|
|
62
|
+
</section>
|
|
63
|
+
<section class="set-panel" data-set-panel="users" hidden>
|
|
64
|
+
<h2 class="text-lg font-semibold">Users</h2>
|
|
65
|
+
<p class="text-sm text-base-content/60 mb-4">Admin 分配账号。对方用同一登录页,没有公开注册。</p>
|
|
66
|
+
<div id="users-list" class="set-list"></div>
|
|
67
|
+
<div class="set-add">
|
|
68
|
+
<input id="user-new-name" class="input input-bordered input-sm" placeholder="username">
|
|
69
|
+
<input id="user-new-pass" type="password" class="input input-bordered input-sm" placeholder="password">
|
|
70
|
+
<select id="user-new-role" class="select select-bordered select-sm">
|
|
71
|
+
<option value="editor">editor</option>
|
|
72
|
+
<option value="viewer">viewer</option>
|
|
73
|
+
<option value="admin">admin</option>
|
|
74
|
+
</select>
|
|
75
|
+
<button type="button" id="user-add-btn" class="btn btn-primary btn-sm">Invite</button>
|
|
19
76
|
</div>
|
|
20
|
-
<
|
|
21
|
-
</
|
|
22
|
-
<
|
|
23
|
-
<
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
77
|
+
<p id="users-error" class="text-error text-xs mt-2 hidden"></p>
|
|
78
|
+
</section>
|
|
79
|
+
<section class="set-panel" data-set-panel="password" hidden>
|
|
80
|
+
<h2 class="text-lg font-semibold">Password</h2>
|
|
81
|
+
<p class="text-sm text-base-content/60 mb-4">改自己的登录密码。管理员重置别人的密码在 Users。</p>
|
|
82
|
+
<label class="form-control mb-2">
|
|
83
|
+
<div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Current</span></div>
|
|
84
|
+
<input id="pw-current" type="password" class="input input-bordered input-sm">
|
|
85
|
+
</label>
|
|
86
|
+
<label class="form-control mb-2">
|
|
87
|
+
<div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">New</span></div>
|
|
88
|
+
<input id="pw-next" type="password" class="input input-bordered input-sm">
|
|
89
|
+
</label>
|
|
90
|
+
<label class="form-control mb-2">
|
|
91
|
+
<div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Again</span></div>
|
|
92
|
+
<input id="pw-next2" type="password" class="input input-bordered input-sm">
|
|
93
|
+
</label>
|
|
94
|
+
<p id="pw-error" class="text-error text-xs mt-2 hidden"></p>
|
|
95
|
+
<button type="button" id="pw-save" class="btn btn-primary btn-sm mt-3">Update password</button>
|
|
96
|
+
</section>
|
|
97
|
+
<section class="set-panel" data-set-panel="mcp" hidden>
|
|
98
|
+
<h2 class="text-lg font-semibold">MCP token</h2>
|
|
99
|
+
<p class="text-sm text-base-content/60 mb-4">发给外部 Agent 的短期凭证,只显示一次。需要先选一台在线机器。</p>
|
|
100
|
+
<label class="form-control mb-3">
|
|
101
|
+
<div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Expiry</span></div>
|
|
102
|
+
<select id="mcp-token-expiry" class="select select-bordered select-sm">
|
|
29
103
|
<option value="86400">24 hours</option>
|
|
30
104
|
<option value="604800">7 days</option>
|
|
31
105
|
<option value="2592000">30 days</option>
|
|
32
106
|
</select>
|
|
107
|
+
</label>
|
|
108
|
+
<button type="button" id="mcp-token-generate" class="btn btn-primary btn-sm">Generate</button>
|
|
109
|
+
<div id="mcp-token-result" class="hidden set-token">
|
|
110
|
+
<p id="mcp-token-url" class="font-mono text-xs text-base-content/50"></p>
|
|
111
|
+
<textarea id="mcp-token-value" readonly class="textarea textarea-bordered font-mono text-xs" rows="5"></textarea>
|
|
112
|
+
<button type="button" id="mcp-token-copy" class="btn btn-ghost btn-xs border border-base-300">Copy token</button>
|
|
33
113
|
</div>
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
<textarea id="mcp-token-value" readonly class="textarea textarea-bordered textarea-xs w-full h-20 font-mono"></textarea>
|
|
37
|
-
<button id="mcp-token-copy" class="btn btn-ghost btn-xs mt-1">Copy token</button>
|
|
38
|
-
</div>
|
|
39
|
-
<div id="mcp-token-error" class="alert alert-error text-xs py-1.5 hidden mt-2"></div>
|
|
40
|
-
</div>
|
|
41
|
-
<div class="modal-action">
|
|
42
|
-
<form method="dialog"><button class="btn btn-ghost btn-sm">Close</button></form>
|
|
43
|
-
</div>
|
|
114
|
+
<p id="mcp-token-error" class="text-error text-xs mt-2 hidden"></p>
|
|
115
|
+
</section>
|
|
44
116
|
</div>
|
|
45
|
-
|
|
46
|
-
|
|
117
|
+
</div>
|
|
118
|
+
</div>`;
|
|
119
|
+
}
|
|
47
120
|
|
|
121
|
+
function bind() {
|
|
122
|
+
$('set-back')?.addEventListener('click', closeSettings);
|
|
123
|
+
document.querySelectorAll('[data-set-sec]').forEach(btn => {
|
|
124
|
+
btn.addEventListener('click', () => showSection(btn.dataset.setSec));
|
|
125
|
+
});
|
|
126
|
+
$('ws-add-btn')?.addEventListener('click', addWorkspace);
|
|
127
|
+
$('ws-new-name')?.addEventListener('keydown', e => { if (e.key === 'Enter') addWorkspace(); });
|
|
128
|
+
$('ws-new-dir')?.addEventListener('keydown', e => { if (e.key === 'Enter') addWorkspace(); });
|
|
129
|
+
$('mcp-token-generate')?.addEventListener('click', generateMcpToken);
|
|
130
|
+
$('mcp-token-copy')?.addEventListener('click', copyMcpToken);
|
|
131
|
+
$('claude-env-save')?.addEventListener('click', saveClaudeEnv);
|
|
132
|
+
$('ae-profile')?.addEventListener('change', () => { void loadClaudeEnv(); });
|
|
133
|
+
$('user-add-btn')?.addEventListener('click', inviteUser);
|
|
134
|
+
$('pw-save')?.addEventListener('click', savePassword);
|
|
135
|
+
document.addEventListener('router:settings', () => openSettings());
|
|
136
|
+
document.addEventListener('router:home', () => {
|
|
137
|
+
if (isSettingsOpen()) closeSettings();
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function showSection(id) {
|
|
142
|
+
activeSection = id || 'claude';
|
|
143
|
+
document.querySelectorAll('[data-set-sec]').forEach(b => b.classList.toggle('is-on', b.dataset.setSec === activeSection));
|
|
144
|
+
document.querySelectorAll('[data-set-panel]').forEach(p => {
|
|
145
|
+
p.hidden = p.dataset.setPanel !== activeSection;
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function openSettings(section) {
|
|
150
|
+
const page = $('settings-page');
|
|
151
|
+
if (!page) return;
|
|
152
|
+
page.hidden = false;
|
|
153
|
+
document.documentElement.classList.add('settings-open');
|
|
154
|
+
showSection(section || activeSection || 'claude');
|
|
48
155
|
renderWsList();
|
|
156
|
+
fillProfileSelect();
|
|
157
|
+
void loadClaudeEnv();
|
|
158
|
+
void loadUsers();
|
|
159
|
+
const mcp = $('mcp-token-settings') || document.querySelector('[data-set-panel="mcp"]');
|
|
160
|
+
if (mcp && !hubMode.peek()) {
|
|
161
|
+
document.querySelector('[data-set-sec="mcp"]')?.classList.add('hidden');
|
|
162
|
+
}
|
|
163
|
+
if ((location.hash || '') !== '#/settings') location.hash = '/settings';
|
|
164
|
+
}
|
|
49
165
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
document.
|
|
54
|
-
|
|
166
|
+
export function closeSettings() {
|
|
167
|
+
const page = $('settings-page');
|
|
168
|
+
if (page) page.hidden = true;
|
|
169
|
+
document.documentElement.classList.remove('settings-open');
|
|
170
|
+
if ((location.hash || '') === '#/settings') location.hash = '';
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function isSettingsOpen() {
|
|
174
|
+
return !$('settings-page')?.hidden;
|
|
55
175
|
}
|
|
56
176
|
|
|
57
177
|
function renderWsList() {
|
|
58
178
|
const el = $('ws-list');
|
|
59
179
|
if (!el) return;
|
|
60
|
-
const wss = workspacesData.peek();
|
|
61
|
-
if (!wss.length) {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
180
|
+
const wss = workspacesData.peek() || [];
|
|
181
|
+
if (!wss.length) {
|
|
182
|
+
el.innerHTML = '<div class="text-xs text-base-content/40">还没有 workspace</div>';
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
el.innerHTML = wss.map((w, i) => `
|
|
186
|
+
<div class="set-row" style="--index:${i}">
|
|
187
|
+
<div class="set-row-body">
|
|
65
188
|
<div class="text-sm font-medium">${esc(w.name)}</div>
|
|
66
|
-
<div class="text-xs font-mono text-base-content/50
|
|
189
|
+
<div class="text-xs font-mono text-base-content/50">${esc(w.configDir)}</div>
|
|
67
190
|
</div>
|
|
68
191
|
${wss.length > 1
|
|
69
|
-
? `<button class="btn btn-ghost btn-xs
|
|
70
|
-
: '<span class="text-
|
|
192
|
+
? `<button type="button" class="btn btn-ghost btn-xs" data-ws-delete="${esc(w.id)}">Remove</button>`
|
|
193
|
+
: '<span class="text-xs text-base-content/40">default</span>'}
|
|
71
194
|
</div>`).join('');
|
|
72
|
-
|
|
73
195
|
el.querySelectorAll('[data-ws-delete]').forEach(btn => {
|
|
74
196
|
btn.addEventListener('click', () => deleteWorkspace(btn.dataset.wsDelete));
|
|
75
197
|
});
|
|
76
198
|
}
|
|
77
199
|
|
|
200
|
+
function fillProfileSelect(prefer) {
|
|
201
|
+
const sel = $('ae-profile');
|
|
202
|
+
if (!sel) return;
|
|
203
|
+
const cur = prefer != null ? prefer : sel.value;
|
|
204
|
+
const wss = workspacesData.peek() || [];
|
|
205
|
+
const names = [...new Set(wss.map(w => w.name).filter(Boolean))];
|
|
206
|
+
sel.innerHTML = `<option value="">Default</option>`
|
|
207
|
+
+ names.map(n => `<option value="${esc(n)}">${esc(n)}</option>`).join('');
|
|
208
|
+
if ([...sel.options].some(o => o.value === cur)) sel.value = cur;
|
|
209
|
+
else sel.value = '';
|
|
210
|
+
}
|
|
211
|
+
|
|
78
212
|
async function addWorkspace() {
|
|
79
|
-
const name
|
|
80
|
-
const dir
|
|
81
|
-
const errEl
|
|
213
|
+
const name = $('ws-new-name').value.trim();
|
|
214
|
+
const dir = $('ws-new-dir').value.trim();
|
|
215
|
+
const errEl = $('ws-error');
|
|
82
216
|
errEl.classList.add('hidden');
|
|
83
|
-
if (!name || !dir) {
|
|
217
|
+
if (!name || !dir) {
|
|
218
|
+
errEl.textContent = '需要 name 和路径';
|
|
219
|
+
errEl.classList.remove('hidden');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
84
222
|
try {
|
|
85
223
|
const ws = await api('POST', '/api/workspaces', { name, configDir: dir });
|
|
86
|
-
|
|
87
|
-
|
|
224
|
+
const next = [...(workspacesData.peek() || []).filter(w => w.id !== ws.id), ws];
|
|
225
|
+
workspacesData.value = next;
|
|
226
|
+
setCachedWorkspaces(next);
|
|
88
227
|
$('ws-new-name').value = '';
|
|
89
|
-
$('ws-new-dir').value
|
|
228
|
+
$('ws-new-dir').value = '';
|
|
90
229
|
renderWsList();
|
|
230
|
+
fillProfileSelect(ws.name);
|
|
231
|
+
await loadClaudeEnv();
|
|
232
|
+
showSection('claude');
|
|
91
233
|
} catch (e) {
|
|
92
234
|
errEl.textContent = e.message;
|
|
93
235
|
errEl.classList.remove('hidden');
|
|
@@ -97,19 +239,124 @@ async function addWorkspace() {
|
|
|
97
239
|
async function deleteWorkspace(id) {
|
|
98
240
|
try {
|
|
99
241
|
await api('DELETE', `/api/workspaces/${id}`);
|
|
100
|
-
|
|
101
|
-
|
|
242
|
+
const next = (workspacesData.peek() || []).filter(w => w.id !== id);
|
|
243
|
+
workspacesData.value = next;
|
|
244
|
+
setCachedWorkspaces(next);
|
|
102
245
|
renderWsList();
|
|
246
|
+
fillProfileSelect('');
|
|
247
|
+
await loadClaudeEnv();
|
|
103
248
|
} catch (e) {
|
|
104
|
-
|
|
249
|
+
const errEl = $('ws-error');
|
|
250
|
+
if (errEl) { errEl.textContent = e.message; errEl.classList.remove('hidden'); }
|
|
105
251
|
}
|
|
106
252
|
}
|
|
107
253
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
254
|
+
async function loadClaudeEnv() {
|
|
255
|
+
try {
|
|
256
|
+
const name = $('ae-profile')?.value || '';
|
|
257
|
+
const q = name ? `?name=${encodeURIComponent(name)}` : '';
|
|
258
|
+
const data = await api('GET', `/api/agent-env${q}`);
|
|
259
|
+
const env = data.env || {};
|
|
260
|
+
const proc = name ? {} : (data.process || {});
|
|
261
|
+
for (const key of CLAUDE_ENV_FIELDS) {
|
|
262
|
+
const el = $(`ae-${key}`);
|
|
263
|
+
if (!el) continue;
|
|
264
|
+
const v = env[key] ?? proc[key];
|
|
265
|
+
if (v && typeof v === 'object' && v.set) {
|
|
266
|
+
el.placeholder = `saved ${v.preview || '••••'}`;
|
|
267
|
+
el.value = '';
|
|
268
|
+
el.dataset.keep = '1';
|
|
269
|
+
} else {
|
|
270
|
+
el.value = typeof v === 'string' ? v : '';
|
|
271
|
+
el.placeholder = key;
|
|
272
|
+
el.dataset.keep = '';
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
} catch { /* older edges */ }
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async function saveClaudeEnv() {
|
|
279
|
+
const errEl = $('claude-env-error');
|
|
280
|
+
errEl?.classList.add('hidden');
|
|
281
|
+
const env = {};
|
|
282
|
+
for (const key of CLAUDE_ENV_FIELDS) {
|
|
283
|
+
const el = $(`ae-${key}`);
|
|
284
|
+
if (!el) continue;
|
|
285
|
+
const val = el.value.trim();
|
|
286
|
+
if (val) env[key] = val;
|
|
287
|
+
else if (el.dataset.keep === '1') env[key] = { set: true };
|
|
288
|
+
else env[key] = '';
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
const name = $('ae-profile')?.value || '';
|
|
292
|
+
await api('PUT', '/api/agent-env', { name, env });
|
|
293
|
+
await loadClaudeEnv();
|
|
294
|
+
} catch (e) {
|
|
295
|
+
if (errEl) { errEl.textContent = e.message; errEl.classList.remove('hidden'); }
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function loadUsers() {
|
|
300
|
+
const nav = $('set-nav-users');
|
|
301
|
+
const panel = document.querySelector('[data-set-panel="users"]');
|
|
302
|
+
try {
|
|
303
|
+
const me = await api('GET', '/api/collab/me');
|
|
304
|
+
const admin = me.role === 'admin' || me.role === 'owner';
|
|
305
|
+
nav?.classList.toggle('hidden', !admin);
|
|
306
|
+
if (panel) panel.hidden = !admin || activeSection !== 'users';
|
|
307
|
+
if (!admin) {
|
|
308
|
+
if (activeSection === 'users') showSection('claude');
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
const users = await api('GET', '/api/collab/users');
|
|
312
|
+
const list = $('users-list');
|
|
313
|
+
const rows = Array.isArray(users) ? users : [];
|
|
314
|
+
list.innerHTML = rows.map(u =>
|
|
315
|
+
`<div class="set-row"><div class="text-sm font-medium flex-1">${esc(u.username)}</div><div class="text-xs text-base-content/50">${esc(u.role || '')}</div></div>`
|
|
316
|
+
).join('') || '<div class="text-xs text-base-content/40">还没有其他用户</div>';
|
|
317
|
+
} catch {
|
|
318
|
+
nav?.classList.add('hidden');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function inviteUser() {
|
|
323
|
+
const errEl = $('users-error');
|
|
324
|
+
errEl?.classList.add('hidden');
|
|
325
|
+
const username = $('user-new-name')?.value.trim();
|
|
326
|
+
const password = $('user-new-pass')?.value || '';
|
|
327
|
+
const role = $('user-new-role')?.value || 'editor';
|
|
328
|
+
if (!username || !password) {
|
|
329
|
+
if (errEl) { errEl.textContent = '需要用户名和密码'; errEl.classList.remove('hidden'); }
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
await api('POST', '/api/collab/users', { username, password, role });
|
|
334
|
+
$('user-new-name').value = '';
|
|
335
|
+
$('user-new-pass').value = '';
|
|
336
|
+
await loadUsers();
|
|
337
|
+
} catch (e) {
|
|
338
|
+
if (errEl) { errEl.textContent = e.message; errEl.classList.remove('hidden'); }
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function savePassword() {
|
|
343
|
+
const errEl = $('pw-error');
|
|
344
|
+
errEl?.classList.add('hidden');
|
|
345
|
+
const current = $('pw-current')?.value || '';
|
|
346
|
+
const next = $('pw-next')?.value || '';
|
|
347
|
+
const next2 = $('pw-next2')?.value || '';
|
|
348
|
+
if (next !== next2) {
|
|
349
|
+
if (errEl) { errEl.textContent = '两次新密码不一致'; errEl.classList.remove('hidden'); }
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
try {
|
|
353
|
+
await api('POST', '/api/auth/password', { current, next });
|
|
354
|
+
$('pw-current').value = '';
|
|
355
|
+
$('pw-next').value = '';
|
|
356
|
+
$('pw-next2').value = '';
|
|
357
|
+
} catch (e) {
|
|
358
|
+
if (errEl) { errEl.textContent = e.message; errEl.classList.remove('hidden'); }
|
|
359
|
+
}
|
|
113
360
|
}
|
|
114
361
|
|
|
115
362
|
async function generateMcpToken() {
|
|
@@ -119,7 +366,7 @@ async function generateMcpToken() {
|
|
|
119
366
|
err?.classList.add('hidden');
|
|
120
367
|
const machineId = selectedMachineId.peek();
|
|
121
368
|
if (!machineId) {
|
|
122
|
-
err.textContent = '
|
|
369
|
+
err.textContent = '请先在主界面选一台在线机器。';
|
|
123
370
|
err.classList.remove('hidden');
|
|
124
371
|
return;
|
|
125
372
|
}
|
|
@@ -127,6 +374,8 @@ async function generateMcpToken() {
|
|
|
127
374
|
const expiresInSeconds = Number($('mcp-token-expiry').value);
|
|
128
375
|
const data = await api('POST', `/api/mcp/tokens?machine=${encodeURIComponent(machineId)}`, { expiresInSeconds });
|
|
129
376
|
$('mcp-token-value').value = data.token || '';
|
|
377
|
+
const urlEl = $('mcp-token-url');
|
|
378
|
+
if (urlEl) urlEl.textContent = `${location.origin}/mcp`;
|
|
130
379
|
result.classList.remove('hidden');
|
|
131
380
|
} catch (e) {
|
|
132
381
|
err.textContent = e.message;
|
package/public/shell/notes.js
CHANGED
|
@@ -104,6 +104,7 @@ export function renderSessionNotesBar() {
|
|
|
104
104
|
const note = (meta.notes || '').trim();
|
|
105
105
|
const fav = !!meta.favorite;
|
|
106
106
|
const hid = !!meta.hidden;
|
|
107
|
+
const branch = meta.branch?.parentSessionId ? meta.branch : null;
|
|
107
108
|
const title = (meta.title || '').trim();
|
|
108
109
|
const showTitle = title || sid.slice(0, 8);
|
|
109
110
|
bar.style.display = 'flex';
|
|
@@ -125,6 +126,7 @@ export function renderSessionNotesBar() {
|
|
|
125
126
|
<span class="font-bold text-base-content/40 uppercase flex-shrink-0">Note:</span>
|
|
126
127
|
<span class="truncate text-base-content/70 ${note ? '' : 'italic text-base-content/40'}"
|
|
127
128
|
id="session-notes-text" title="${esc(note || 'Add a note for this session')}">${esc(note || 'No session note')}</span>
|
|
129
|
+
${branch ? `<span class="session-branch-current" title="Forked from ${esc(branch.parentAgent || agent)} · ${esc(branch.parentSessionId)}">⎇ from ${esc(branch.parentSessionId.slice(0, 8))}</span>` : ''}
|
|
128
130
|
</div>
|
|
129
131
|
<button id="btn-edit-session-notes" class="btn btn-ghost btn-xs px-1.5">Note</button>
|
|
130
132
|
`;
|
|
@@ -147,7 +149,7 @@ export function renderSessionNotesEditMode() {
|
|
|
147
149
|
}
|
|
148
150
|
|
|
149
151
|
function emptyMeta() {
|
|
150
|
-
return { favorite: false, notes: '', title: '', hidden: false };
|
|
152
|
+
return { favorite: false, notes: '', title: '', hidden: false, branch: null };
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
function mergeMeta(prev, patch) {
|
|
@@ -156,12 +158,13 @@ function mergeMeta(prev, patch) {
|
|
|
156
158
|
notes: patch.notes !== undefined ? String(patch.notes || '') : String(prev.notes || ''),
|
|
157
159
|
title: patch.title !== undefined ? String(patch.title || '').trim() : String(prev.title || '').trim(),
|
|
158
160
|
hidden: patch.hidden !== undefined ? Boolean(patch.hidden) : Boolean(prev.hidden),
|
|
161
|
+
branch: patch.branch !== undefined ? (patch.branch || null) : (prev.branch || null),
|
|
159
162
|
updatedAt: Date.now(),
|
|
160
163
|
};
|
|
161
164
|
}
|
|
162
165
|
|
|
163
166
|
function isMetaEmpty(m) {
|
|
164
|
-
return !m?.favorite && !m?.hidden && !(m?.notes || '').trim() && !(m?.title || '').trim();
|
|
167
|
+
return !m?.favorite && !m?.hidden && !(m?.notes || '').trim() && !(m?.title || '').trim() && !m?.branch;
|
|
165
168
|
}
|
|
166
169
|
|
|
167
170
|
export async function toggleSessionFavorite(sessionId, agent, { machineId } = {}) {
|
|
@@ -23,6 +23,45 @@ function normalizeSessions(v) {
|
|
|
23
23
|
return [];
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
function branchParentKey(branch, session) {
|
|
27
|
+
if (!branch?.parentSessionId) return '';
|
|
28
|
+
return metaKey(branch.parentAgent || session.agent, branch.parentSessionId,
|
|
29
|
+
branch.parentMachineId ?? session.machineId);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Keep fork descendants adjacent to their source so the sidebar is a small,
|
|
33
|
+
* readable conversation tree instead of a flat chronology. */
|
|
34
|
+
function orderBranchSessions(sessions) {
|
|
35
|
+
const list = Array.isArray(sessions) ? sessions : [];
|
|
36
|
+
const byKey = new Map(list.map(s => [metaKey(s.agent, s.sessionId, s.machineId), s]));
|
|
37
|
+
const children = new Map();
|
|
38
|
+
const roots = [];
|
|
39
|
+
for (const s of list) {
|
|
40
|
+
const branch = sessionMetaMap.peek()?.[metaKey(s.agent, s.sessionId, s.machineId)]?.branch;
|
|
41
|
+
const parent = branchParentKey(branch, s);
|
|
42
|
+
if (parent && byKey.has(parent) && parent !== metaKey(s.agent, s.sessionId, s.machineId)) {
|
|
43
|
+
const rows = children.get(parent) || [];
|
|
44
|
+
rows.push(s);
|
|
45
|
+
children.set(parent, rows);
|
|
46
|
+
} else {
|
|
47
|
+
roots.push(s);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const byRecent = (a, b) => (b.updatedAt || 0) - (a.updatedAt || 0);
|
|
51
|
+
roots.sort(byRecent);
|
|
52
|
+
for (const rows of children.values()) rows.sort(byRecent);
|
|
53
|
+
const out = [];
|
|
54
|
+
const visit = (s, depth, seen = new Set()) => {
|
|
55
|
+
const key = metaKey(s.agent, s.sessionId, s.machineId);
|
|
56
|
+
if (seen.has(key)) return;
|
|
57
|
+
seen.add(key);
|
|
58
|
+
out.push(depth ? { ...s, _branchDepth: depth } : s);
|
|
59
|
+
for (const child of children.get(key) || []) visit(child, depth + 1, seen);
|
|
60
|
+
};
|
|
61
|
+
for (const root of roots) visit(root, 0);
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
|
|
26
65
|
function sessionsQuery({ hubIndex = false } = {}) {
|
|
27
66
|
const q = new URLSearchParams();
|
|
28
67
|
// Project index is small and should remain complete; the Hub/browser cache
|
|
@@ -97,6 +136,8 @@ export function sessionItemHtml(s, { showProject = false } = {}) {
|
|
|
97
136
|
const fav = !!meta.favorite;
|
|
98
137
|
const hid = !!meta.hidden;
|
|
99
138
|
const hasNote = !!(meta.notes && String(meta.notes).trim());
|
|
139
|
+
const branch = meta.branch?.parentSessionId ? meta.branch : null;
|
|
140
|
+
const branchDepth = Math.max(0, Math.min(4, Number(s._branchDepth) || 0));
|
|
100
141
|
const turns = Number(s.turnCount) || 0;
|
|
101
142
|
const thin = turns > 0 && turns <= LOW_TURN_THRESHOLD;
|
|
102
143
|
const projectLine = showProject
|
|
@@ -107,6 +148,7 @@ export function sessionItemHtml(s, { showProject = false } = {}) {
|
|
|
107
148
|
renamed ? `<span class="session-renamed-badge" title="Custom name (original: ${esc(s.display || '')})">renamed</span>` : '',
|
|
108
149
|
hid ? `<span class="session-hidden-badge" title="Hidden">hidden</span>` : '',
|
|
109
150
|
hasNote ? `<span class="session-note-dot" title="${esc(meta.notes)}">📝</span>` : '',
|
|
151
|
+
branch ? `<span class="session-branch-badge" title="Forked from ${esc(branch.parentAgent || s.agent)}:${esc(branch.parentSessionId)}">⎇ fork</span>` : '',
|
|
110
152
|
].filter(Boolean).join('');
|
|
111
153
|
const machineBadge = s.machineId
|
|
112
154
|
? `<span class="text-[9px] px-1 py-0.5 rounded bg-base-300 text-base-content/50 font-mono flex-shrink-0" title="Machine">${esc(s.machineId)}</span>`
|
|
@@ -114,8 +156,12 @@ export function sessionItemHtml(s, { showProject = false } = {}) {
|
|
|
114
156
|
const tip = renamed
|
|
115
157
|
? `${title}\n(original: ${s.display || s.sessionId})`
|
|
116
158
|
: (s.display || '');
|
|
117
|
-
|
|
159
|
+
const branchLine = branch
|
|
160
|
+
? `<div class="session-branch-line" title="Forked from ${esc(branch.parentAgent || s.agent)} · ${esc(branch.parentSessionId)}">↳ ${esc(branch.parentAgent || s.agent)} · ${esc(branch.parentSessionId.slice(0, 8))}</div>`
|
|
161
|
+
: '';
|
|
162
|
+
return `<div class="session-row ${branchDepth ? 'session-branch' : ''} px-2.5 py-1.5 cursor-pointer hover:bg-base-300 border-l-2
|
|
118
163
|
${active ? 'border-primary bg-primary/5' : 'border-transparent'} ${fav ? 'session-fav' : ''} ${hid ? 'session-hidden-row' : ''} ${thin ? 'session-thin' : ''}"
|
|
164
|
+
style="--branch-depth:${branchDepth}"
|
|
119
165
|
data-session-id="${esc(s.sessionId)}"
|
|
120
166
|
data-session-cwd="${esc(s.cwd || '')}"
|
|
121
167
|
data-session-config-dir="${esc(s.configDir || '')}"
|
|
@@ -149,6 +195,7 @@ export function sessionItemHtml(s, { showProject = false } = {}) {
|
|
|
149
195
|
title="${hid ? 'Unhide session' : 'Hide session'}">${hid ? '👁' : '🙈'}</button>
|
|
150
196
|
</div>
|
|
151
197
|
${projectLine}
|
|
198
|
+
${branchLine}
|
|
152
199
|
${badges || machineBadge ? `<div class="flex items-center gap-1 mt-0.5 flex-wrap">${machineBadge}${badges}</div>` : ''}
|
|
153
200
|
</div>
|
|
154
201
|
<span class="text-[9px] text-base-content/30 flex-shrink-0 mt-0.5">${formatTime(s.updatedAt)}</span>
|
|
@@ -290,7 +337,8 @@ export function renderProjectGroups(groups) {
|
|
|
290
337
|
<span class="text-[9px] text-base-content/30 flex-shrink-0">${formatTime(g.updatedAt)}</span>
|
|
291
338
|
</div>
|
|
292
339
|
</div>`;
|
|
293
|
-
const
|
|
340
|
+
const ordered = orderBranchSessions(g.sessions);
|
|
341
|
+
const sessions = ordered.slice(0, 40);
|
|
294
342
|
const more = g.sessions.length > 40
|
|
295
343
|
? `<div class="px-3 py-1 text-[10px] text-base-content/30">+${g.sessions.length - 40} more</div>` : '';
|
|
296
344
|
const body = open
|