claude-simple 0.2.3 → 0.2.5
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 +1 -0
- package/bin/edge-ctl.js +260 -0
- package/client.js +7 -1
- package/dist/app.0.2.5.min.js +953 -0
- package/dist/index.html +2 -2
- package/dist/{style.0.2.3.min.css → style.0.2.5.min.css} +229 -32
- package/package.json +6 -2
- package/public/app.js +12 -2
- package/public/chat.js +271 -61
- package/public/index.html +33 -3
- package/public/mobile.js +72 -0
- package/public/pwa.js +32 -2
- package/public/shell/boot.js +2 -0
- package/public/shell/hub.js +5 -1
- package/public/shell/layout.js +11 -2
- package/public/shell/templates.js +37 -19
- package/public/shell.js +34 -4
- package/public/state.js +8 -1
- package/public/style.css +228 -31
- package/public/sw.js +2 -2
- package/src/app.js +21 -7
- package/dist/app.0.2.3.min.js +0 -924
package/public/shell/boot.js
CHANGED
|
@@ -14,12 +14,14 @@ import {
|
|
|
14
14
|
refreshMachinesList, hideMachinePicker, showMachinePicker,
|
|
15
15
|
syncTopbarMachine, startMachinePolling,
|
|
16
16
|
} from './hub.js';
|
|
17
|
+
import { syncFocusScope } from '../mobile.js';
|
|
17
18
|
|
|
18
19
|
export async function showApp(opts = {}) {
|
|
19
20
|
$('auth-screen').style.display = 'none';
|
|
20
21
|
const app = $('app');
|
|
21
22
|
app.classList.remove('hidden');
|
|
22
23
|
app.style.display = 'flex';
|
|
24
|
+
queueMicrotask(syncFocusScope);
|
|
23
25
|
|
|
24
26
|
// Use probe result from boot when provided (avoid second /api/hub round-trip)
|
|
25
27
|
let hub = opts.hub;
|
package/public/shell/hub.js
CHANGED
|
@@ -8,6 +8,7 @@ import { api, probeHub } from '../api.js';
|
|
|
8
8
|
import { loadAllSessions, loadWorkspaces, loadAgentsMeta } from './session-list.js';
|
|
9
9
|
import { loadSessionMetaMap } from './notes.js';
|
|
10
10
|
import { loadMetaAgentBoot } from '../agent-panel.js';
|
|
11
|
+
import { syncFocusScope } from '../mobile.js';
|
|
11
12
|
// goHome imported dynamically in enterMachine to avoid circular deps
|
|
12
13
|
|
|
13
14
|
export async function refreshMachinesList() {
|
|
@@ -54,6 +55,9 @@ export function showMachinePicker() {
|
|
|
54
55
|
const el = $('machine-picker');
|
|
55
56
|
if (!el) return;
|
|
56
57
|
el.classList.remove('hidden');
|
|
58
|
+
// On phones hidden panels are inert so Safari's form assistant cannot tab
|
|
59
|
+
// into them. Re-enable this picker before its machine buttons are shown.
|
|
60
|
+
syncFocusScope();
|
|
57
61
|
hubMachineReady.value = false;
|
|
58
62
|
renderMachinePickerList();
|
|
59
63
|
const status = $('machine-picker-status');
|
|
@@ -65,6 +69,7 @@ export function showMachinePicker() {
|
|
|
65
69
|
|
|
66
70
|
export function hideMachinePicker() {
|
|
67
71
|
$('machine-picker')?.classList.add('hidden');
|
|
72
|
+
syncFocusScope();
|
|
68
73
|
hubMachineReady.value = true;
|
|
69
74
|
}
|
|
70
75
|
|
|
@@ -191,4 +196,3 @@ export async function enterMachine(machineId, { forceReload = false } = {}) {
|
|
|
191
196
|
await Promise.all([loadAllSessions(), loadWorkspaces(), loadSessionMetaMap(), loadAgentsMeta().catch(() => {})]);
|
|
192
197
|
hubMachineReady.value = true;
|
|
193
198
|
}
|
|
194
|
-
|
package/public/shell/layout.js
CHANGED
|
@@ -12,10 +12,19 @@ import { connectWS, clearMessages, appendSystemMsg, applyChatDensity } from '../
|
|
|
12
12
|
import { refreshModelSelect } from './session-list.js';
|
|
13
13
|
import { switchTab } from './session-nav.js';
|
|
14
14
|
import { showMachinePicker } from './hub.js';
|
|
15
|
+
import { syncFocusScope } from '../mobile.js';
|
|
15
16
|
|
|
16
17
|
// ── Sidebar ───────────────────────────────────────────────────────────────────
|
|
17
|
-
export function openSidebar() {
|
|
18
|
-
|
|
18
|
+
export function openSidebar() {
|
|
19
|
+
$('sidebar').classList.add('open');
|
|
20
|
+
$('sidebar-overlay').classList.remove('hidden');
|
|
21
|
+
syncFocusScope();
|
|
22
|
+
}
|
|
23
|
+
export function closeSidebar() {
|
|
24
|
+
$('sidebar').classList.remove('open');
|
|
25
|
+
$('sidebar-overlay').classList.add('hidden');
|
|
26
|
+
syncFocusScope();
|
|
27
|
+
}
|
|
19
28
|
export function closeSidebarOnMobile() { if (window.innerWidth <= 640) closeSidebar(); }
|
|
20
29
|
|
|
21
30
|
export function initSidebarToggle() {
|
|
@@ -7,6 +7,7 @@ export const AuthScreen = () => `
|
|
|
7
7
|
<div class="card-body gap-3 p-8">
|
|
8
8
|
<h1 class="text-xl font-bold">🤖 Agent UI</h1>
|
|
9
9
|
<p id="auth-subtitle" class="text-sm text-base-content/60 mb-1">Claude · Codex · Grok</p>
|
|
10
|
+
<form id="auth-form" class="flex flex-col gap-3" autocomplete="on" action="#">
|
|
10
11
|
<label class="form-control">
|
|
11
12
|
<div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Username</span></div>
|
|
12
13
|
<input id="auth-username" type="text" class="input input-bordered input-sm" placeholder="username" autocomplete="username">
|
|
@@ -15,7 +16,8 @@ export const AuthScreen = () => `
|
|
|
15
16
|
<div class="label py-1"><span class="label-text text-xs uppercase tracking-wide">Password</span></div>
|
|
16
17
|
<input id="auth-password" type="password" class="input input-bordered input-sm" placeholder="••••••" autocomplete="current-password">
|
|
17
18
|
</label>
|
|
18
|
-
<button id="auth-btn" class="btn btn-primary btn-sm mt-1" data-mode="login">Sign In</button>
|
|
19
|
+
<button type="submit" id="auth-btn" class="btn btn-primary btn-sm mt-1" data-mode="login">Sign In</button>
|
|
20
|
+
</form>
|
|
19
21
|
<div id="auth-error" class="alert alert-error text-xs py-2 hidden"></div>
|
|
20
22
|
</div>
|
|
21
23
|
</div>
|
|
@@ -72,8 +74,8 @@ export const AppShell = (opts = {}) => {
|
|
|
72
74
|
<div class="flex gap-2 topbar-actions">
|
|
73
75
|
<button id="btn-refresh-conn" class="btn btn-ghost btn-xs border border-base-300" title="重新连接并同步">↻</button>
|
|
74
76
|
<button id="btn-pwa-install" class="btn btn-ghost btn-xs border border-base-300 hidden" title="安装到主屏幕">安装</button>
|
|
75
|
-
<button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary" title="Meta Agent — 报告 / 问答 / VLM">✦ Agent</button>
|
|
76
|
-
<button id="btn-new-session" class="btn btn-ghost btn-xs border border-base-300">+ New</button>
|
|
77
|
+
<button id="btn-meta-agent" class="btn btn-ghost btn-xs border border-primary/40 text-primary" title="Meta Agent — 报告 / 问答 / VLM"><span class="hidden sm:inline">✦ Agent</span><span class="sm:hidden">✦</span></button>
|
|
78
|
+
<button id="btn-new-session" class="btn btn-ghost btn-xs border border-base-300" title="New session"><span class="hidden sm:inline">+ New</span><span class="sm:hidden">+</span></button>
|
|
77
79
|
<button id="btn-theme" class="btn btn-ghost btn-xs border border-base-300">🌙</button>
|
|
78
80
|
<button id="btn-settings" class="btn btn-ghost btn-xs border border-base-300">⚙️</button>
|
|
79
81
|
<button id="btn-logout" class="btn btn-ghost btn-xs border border-base-300">Sign out</button>
|
|
@@ -238,8 +240,10 @@ export const AppShell = (opts = {}) => {
|
|
|
238
240
|
<button id="scroll-bottom-btn" class="scroll-fab hidden" title="Jump to bottom">↓</button>
|
|
239
241
|
</div>
|
|
240
242
|
<div id="permission-requests"></div>
|
|
243
|
+
<form id="chat-form" autocomplete="off" action="#" class="flex flex-col flex-shrink-0">
|
|
241
244
|
<div id="chat-composer" class="flex flex-col border-t border-base-300 bg-base-200 flex-shrink-0">
|
|
242
|
-
<div id="chat-
|
|
245
|
+
<div id="chat-attach-bar" class="hidden flex flex-wrap gap-1.5 px-3 pt-2"></div>
|
|
246
|
+
<div id="chat-opts" class="hidden flex items-center gap-1.5 px-3 pt-2 flex-wrap" inert>
|
|
243
247
|
<select id="sel-agent" class="select select-xs select-bordered text-xs font-semibold" title="Agent">
|
|
244
248
|
<option value="claude">Claude</option>
|
|
245
249
|
<option value="codex">Codex</option>
|
|
@@ -282,23 +286,37 @@ export const AppShell = (opts = {}) => {
|
|
|
282
286
|
<option value="full">density: full</option>
|
|
283
287
|
</select>
|
|
284
288
|
</div>
|
|
285
|
-
<div id="composer-row" class="
|
|
286
|
-
<
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
289
|
+
<div id="composer-row" class="px-3 pt-2 pb-2">
|
|
290
|
+
<div class="composer-surface">
|
|
291
|
+
<textarea id="chat-input" rows="1" placeholder="问点什么…"
|
|
292
|
+
class="chat-input w-full text-sm resize-none leading-relaxed"
|
|
293
|
+
name="q"
|
|
294
|
+
enterkeyhint="send"
|
|
295
|
+
inputmode="text"
|
|
296
|
+
autocomplete="off"
|
|
297
|
+
autocorrect="off"
|
|
298
|
+
autocapitalize="sentences"
|
|
299
|
+
spellcheck="false"
|
|
300
|
+
data-1p-ignore="true"
|
|
301
|
+
data-lpignore="true"
|
|
302
|
+
data-form-type="other"
|
|
303
|
+
data-bwignore="true"></textarea>
|
|
304
|
+
<div class="composer-actions">
|
|
305
|
+
<div class="composer-tools">
|
|
306
|
+
<button type="button" id="btn-opts" class="composer-icon-btn" title="Options" aria-label="Options">⚙</button>
|
|
307
|
+
<button type="button" id="btn-attach" class="composer-icon-btn" title="添加图片 / 文件" aria-label="添加图片 / 文件">📎</button>
|
|
308
|
+
<input type="file" id="chat-file-input" class="hidden" multiple accept="image/*,*/*" tabindex="-1">
|
|
309
|
+
</div>
|
|
310
|
+
<div class="composer-submit">
|
|
311
|
+
<span class="composer-hint">Enter 发送</span>
|
|
312
|
+
<button type="submit" id="send-btn" class="composer-send-btn">Send</button>
|
|
313
|
+
<button type="button" id="stop-btn" class="composer-stop-btn hidden" aria-label="停止生成">■</button>
|
|
314
|
+
</div>
|
|
315
|
+
</div>
|
|
316
|
+
</div>
|
|
300
317
|
</div>
|
|
301
318
|
</div>
|
|
319
|
+
</form>
|
|
302
320
|
</div>
|
|
303
321
|
<div id="tab-files" class="hidden flex-col flex-1 overflow-hidden">
|
|
304
322
|
<div class="flex items-center gap-2 px-3 py-1.5 border-b border-base-300 flex-shrink-0 bg-base-200">
|
package/public/shell.js
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from './state.js';
|
|
15
15
|
import { api } from './api.js';
|
|
16
16
|
import { promptPwaInstall } from './pwa.js';
|
|
17
|
+
import { syncFocusScope } from './mobile.js';
|
|
17
18
|
import { getLastSessionContext } from './shell/session-context.js';
|
|
18
19
|
import {
|
|
19
20
|
connectWS, clearMessages, appendMsg, appendSystemMsg, sendMessage, stopProcessing,
|
|
@@ -66,7 +67,8 @@ function showPwaInstallHelp() {
|
|
|
66
67
|
<li>下滑找到并点 <strong>添加到主屏幕</strong></li>
|
|
67
68
|
<li>确认名称后点 <strong>添加</strong></li>
|
|
68
69
|
</ol>
|
|
69
|
-
<p class="text-xs text-base-content/50 mt-3">需用 <strong>Safari</strong> 打开本站;微信/Chrome 内置浏览器通常没有「添加到主屏幕」。</p
|
|
70
|
+
<p class="text-xs text-base-content/50 mt-3">需用 <strong>Safari</strong> 打开本站;微信/Chrome 内置浏览器通常没有「添加到主屏幕」。</p>
|
|
71
|
+
<p class="text-xs text-warning/80 mt-2">当前若是 IP + 自签 HTTPS,Safari 仍会显示「不安全」。加到主屏幕可去掉浏览器外壳,但不能去掉系统输入条。彻底干净需要正式域名证书。</p>`;
|
|
70
72
|
} else if (isAndroid) {
|
|
71
73
|
body = `<ol class="list-decimal pl-4 space-y-2 text-sm text-base-content/80">
|
|
72
74
|
<li>点右上角菜单 <strong>⋮</strong></li>
|
|
@@ -107,8 +109,30 @@ export async function initAuth() {
|
|
|
107
109
|
app.classList.add('hidden');
|
|
108
110
|
app.style.display = 'none';
|
|
109
111
|
}
|
|
110
|
-
const status = await api('GET', '/api/auth/status');
|
|
111
112
|
const btn = $('auth-btn');
|
|
113
|
+
let status;
|
|
114
|
+
const controller = new AbortController();
|
|
115
|
+
const timer = setTimeout(() => controller.abort(), 8000);
|
|
116
|
+
try {
|
|
117
|
+
// This is the first request made by a cold PWA launch. Bound it so a
|
|
118
|
+
// captive portal, stale reverse proxy, or sleeping hub cannot strand the
|
|
119
|
+
// app behind its boot screen indefinitely.
|
|
120
|
+
status = await api('GET', '/api/auth/status', undefined, controller.signal);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
console.warn('[auth status]', e);
|
|
123
|
+
const error = $('auth-error');
|
|
124
|
+
if (error) {
|
|
125
|
+
error.textContent = '无法连接服务器。请检查网络或服务地址后重试。';
|
|
126
|
+
error.classList.remove('hidden');
|
|
127
|
+
}
|
|
128
|
+
// The normal sign-in form remains usable: a transient status failure
|
|
129
|
+
// should be visible, not turn the installed app into a black screen.
|
|
130
|
+
if (btn) btn.dataset.mode = 'login';
|
|
131
|
+
return;
|
|
132
|
+
} finally {
|
|
133
|
+
clearTimeout(timer);
|
|
134
|
+
}
|
|
135
|
+
|
|
112
136
|
if (status.needsSetup) {
|
|
113
137
|
$('auth-subtitle').textContent = 'Create your account to get started';
|
|
114
138
|
btn.textContent = 'Create Account';
|
|
@@ -276,6 +300,7 @@ export function initShell(opts = {}) {
|
|
|
276
300
|
document.dispatchEvent(new CustomEvent('shell-tab-shown'));
|
|
277
301
|
});
|
|
278
302
|
}
|
|
303
|
+
syncFocusScope();
|
|
279
304
|
});
|
|
280
305
|
|
|
281
306
|
initSidebarResize();
|
|
@@ -307,6 +332,7 @@ export function initShell(opts = {}) {
|
|
|
307
332
|
|
|
308
333
|
delegate.on('click', '#btn-home', goHome);
|
|
309
334
|
delegate.on('click', '#auth-btn', submitAuth);
|
|
335
|
+
delegate.on('submit', '#auth-form', e => { e.preventDefault(); submitAuth(); });
|
|
310
336
|
delegate.on('keydown', '#auth-username, #auth-password', e => { if (e.key === 'Enter') submitAuth(); });
|
|
311
337
|
delegate.on('click', '#hamburger', () => $('sidebar').classList.contains('open') ? closeSidebar() : openSidebar());
|
|
312
338
|
delegate.on('click', '#sidebar-overlay', closeSidebar);
|
|
@@ -460,7 +486,10 @@ export function initShell(opts = {}) {
|
|
|
460
486
|
delegate.on('click', '#git-root-btn', () => applyRoot('git-root-input', gitRoot));
|
|
461
487
|
delegate.on('keydown', '#files-root-input', e => { if (e.key === 'Enter') applyRoot('files-root-input', filesRoot); });
|
|
462
488
|
delegate.on('keydown', '#git-root-input', e => { if (e.key === 'Enter') applyRoot('git-root-input', gitRoot); });
|
|
463
|
-
delegate.on('click', '#btn-opts', () => {
|
|
489
|
+
delegate.on('click', '#btn-opts', () => {
|
|
490
|
+
$('chat-opts').classList.toggle('hidden');
|
|
491
|
+
syncFocusScope();
|
|
492
|
+
});
|
|
464
493
|
|
|
465
494
|
delegate.on('change', '#sel-agent', (_, el) => {
|
|
466
495
|
const prev = currentAgent.peek();
|
|
@@ -470,6 +499,7 @@ export function initShell(opts = {}) {
|
|
|
470
499
|
setAgent(next);
|
|
471
500
|
currentModel.value = getDefaultModel(next) || currentModel.peek();
|
|
472
501
|
localStorage.setItem('model', currentModel.peek());
|
|
502
|
+
$('sel-permission').value = currentPermission.peek();
|
|
473
503
|
refreshModelSelect();
|
|
474
504
|
appendSystemMsg(`Agent → ${AGENT_LABELS[next] || next} · model ${currentModel.peek()} · new session`);
|
|
475
505
|
}
|
|
@@ -700,6 +730,7 @@ export function initShell(opts = {}) {
|
|
|
700
730
|
});
|
|
701
731
|
|
|
702
732
|
refreshModelSelect();
|
|
733
|
+
$('sel-permission').value = currentPermission.peek();
|
|
703
734
|
applyChatDensity();
|
|
704
735
|
const dens = $('sel-density');
|
|
705
736
|
if (dens) dens.value = chatDensity.peek() || 'normal';
|
|
@@ -718,4 +749,3 @@ export function initShell(opts = {}) {
|
|
|
718
749
|
btn.classList.toggle('text-base-content/50', !on);
|
|
719
750
|
});
|
|
720
751
|
}
|
|
721
|
-
|
package/public/state.js
CHANGED
|
@@ -18,7 +18,11 @@ export const wsStatus = signal('idle');
|
|
|
18
18
|
export const currentAgent = signal(localStorage.getItem('agent') || 'claude'); // 'claude'|'codex'|'grok'
|
|
19
19
|
export const currentModel = signal(localStorage.getItem('model') || 'claude-sonnet-4-5');
|
|
20
20
|
export const currentEffort = signal('');
|
|
21
|
-
|
|
21
|
+
// Codex is intentionally trusted by default; users can still choose another
|
|
22
|
+
// mode from the expanded composer options before starting a turn.
|
|
23
|
+
export const currentPermission = signal(
|
|
24
|
+
currentAgent.peek() === 'codex' ? 'bypassPermissions' : 'default'
|
|
25
|
+
); // 'default'|'acceptEdits'|'bypassPermissions'|'plan'|'auto'
|
|
22
26
|
export const filesRoot = signal(''); // custom root override for Files tab
|
|
23
27
|
export const gitRoot = signal(''); // custom root override for Git tab
|
|
24
28
|
export const agentsMeta = signal(null); // from /api/agents (dynamic model lists)
|
|
@@ -247,6 +251,9 @@ export function setAgent(agent) {
|
|
|
247
251
|
const a = (agent || 'claude').toLowerCase();
|
|
248
252
|
currentAgent.value = a;
|
|
249
253
|
localStorage.setItem('agent', a);
|
|
254
|
+
currentPermission.value = a === 'codex' ? 'bypassPermissions' : 'default';
|
|
255
|
+
const permissionSelect = typeof document === 'undefined' ? null : document.getElementById('sel-permission');
|
|
256
|
+
if (permissionSelect) permissionSelect.value = currentPermission.peek();
|
|
250
257
|
const models = getModelsForAgent(a);
|
|
251
258
|
const def = getDefaultModel(a);
|
|
252
259
|
if (!models.some(m => m.value === currentModel.peek())) {
|
package/public/style.css
CHANGED
|
@@ -13,28 +13,21 @@
|
|
|
13
13
|
#sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,.4); }
|
|
14
14
|
|
|
15
15
|
/* Compact chat composer on phones — less height, less dead space under bar */
|
|
16
|
-
#composer-row {
|
|
17
|
-
|
|
18
|
-
padding: 0.4rem 0.55rem 0.35rem !important;
|
|
19
|
-
}
|
|
16
|
+
#composer-row { padding: 0 !important; }
|
|
17
|
+
.composer-surface { min-height: 0; border-radius: 1.35rem; }
|
|
20
18
|
#chat-input.chat-input,
|
|
21
19
|
#chat-input {
|
|
22
|
-
min-height:
|
|
23
|
-
max-height:
|
|
24
|
-
padding: 0
|
|
25
|
-
border-radius: 0
|
|
20
|
+
min-height: 32px !important;
|
|
21
|
+
max-height: 72px !important;
|
|
22
|
+
padding: 0 !important;
|
|
23
|
+
border-radius: 1.15rem 1.15rem 0 0;
|
|
26
24
|
/* 16px prevents iOS Safari auto-zoom on focus */
|
|
27
25
|
font-size: 16px !important;
|
|
28
|
-
line-height: 1.
|
|
29
|
-
}
|
|
30
|
-
#btn-opts,
|
|
31
|
-
#send-btn,
|
|
32
|
-
#stop-btn {
|
|
33
|
-
height: 36px !important;
|
|
34
|
-
min-height: 36px !important;
|
|
35
|
-
padding-left: 0.65rem;
|
|
36
|
-
padding-right: 0.65rem;
|
|
26
|
+
line-height: 1.4 !important;
|
|
37
27
|
}
|
|
28
|
+
.composer-icon-btn { width: 40px; height: 40px; font-size: 19px; }
|
|
29
|
+
.composer-send-btn, .composer-stop-btn { min-height: 40px; height: 40px; }
|
|
30
|
+
.composer-hint { display: none; }
|
|
38
31
|
#chat-opts {
|
|
39
32
|
padding-top: 0.35rem !important;
|
|
40
33
|
padding-left: 0.55rem !important;
|
|
@@ -48,6 +41,64 @@
|
|
|
48
41
|
.file-row-action {
|
|
49
42
|
opacity: 1 !important;
|
|
50
43
|
}
|
|
44
|
+
|
|
45
|
+
/* Tighter chrome so the thread is the product */
|
|
46
|
+
#topbar-project { display: none; }
|
|
47
|
+
#project-view > .flex.bg-base-200 { /* tab strip */
|
|
48
|
+
min-height: 40px;
|
|
49
|
+
}
|
|
50
|
+
#project-view [data-tab] {
|
|
51
|
+
padding: 0.55rem 0.85rem !important;
|
|
52
|
+
font-size: 13px !important;
|
|
53
|
+
}
|
|
54
|
+
#project-notes-bar,
|
|
55
|
+
#session-notes-bar {
|
|
56
|
+
font-size: 11px;
|
|
57
|
+
padding: 0.35rem 0.7rem !important;
|
|
58
|
+
}
|
|
59
|
+
#messages {
|
|
60
|
+
padding: 0.65rem 0.45rem !important;
|
|
61
|
+
gap: 0.55rem !important;
|
|
62
|
+
}
|
|
63
|
+
.msg-row { gap: 3px; }
|
|
64
|
+
.msg-row.assistant > div:not(.msg-ts),
|
|
65
|
+
.msg-row.error > div:not(.msg-ts) {
|
|
66
|
+
max-width: 100% !important;
|
|
67
|
+
border-radius: 1rem !important;
|
|
68
|
+
border-top-left-radius: 0.3rem !important;
|
|
69
|
+
}
|
|
70
|
+
.msg-row.user > div:not(.msg-ts) {
|
|
71
|
+
max-width: 88% !important;
|
|
72
|
+
border-radius: 1rem !important;
|
|
73
|
+
border-top-right-radius: 0.3rem !important;
|
|
74
|
+
}
|
|
75
|
+
#sidebar-resize,
|
|
76
|
+
#sidebar-toggle {
|
|
77
|
+
display: none !important;
|
|
78
|
+
}
|
|
79
|
+
.md {
|
|
80
|
+
font-size: 0.9375rem;
|
|
81
|
+
line-height: 1.6;
|
|
82
|
+
}
|
|
83
|
+
.md h1, .md h2, .md h3 {
|
|
84
|
+
font-size: 1.02em !important;
|
|
85
|
+
padding: 0.05em 0 0.15em 0.45em !important;
|
|
86
|
+
border-left-width: 3px !important;
|
|
87
|
+
background: none !important;
|
|
88
|
+
border-bottom: none !important;
|
|
89
|
+
margin: 0.7em 0 0.35em !important;
|
|
90
|
+
}
|
|
91
|
+
.md :not(pre) > code {
|
|
92
|
+
white-space: nowrap;
|
|
93
|
+
display: inline;
|
|
94
|
+
font-size: 0.82em;
|
|
95
|
+
padding: 0.08em 0.32em;
|
|
96
|
+
}
|
|
97
|
+
.token-bar {
|
|
98
|
+
font-size: 10px;
|
|
99
|
+
line-height: 1.45;
|
|
100
|
+
padding: 0.15rem 0.45rem;
|
|
101
|
+
}
|
|
51
102
|
}
|
|
52
103
|
|
|
53
104
|
/* File row actions: hover-reveal on desktop only */
|
|
@@ -72,6 +123,20 @@ html, body {
|
|
|
72
123
|
max-height: 100dvh;
|
|
73
124
|
overflow: hidden;
|
|
74
125
|
}
|
|
126
|
+
/* iOS keyboard: shrink the app to the visual viewport so the composer
|
|
127
|
+
* sits above the keyboard instead of floating over Safari chrome. */
|
|
128
|
+
html.vv-keyboard,
|
|
129
|
+
html.vv-keyboard body {
|
|
130
|
+
height: var(--vvh, 100dvh);
|
|
131
|
+
max-height: var(--vvh, 100dvh);
|
|
132
|
+
}
|
|
133
|
+
html.vv-keyboard body {
|
|
134
|
+
position: fixed;
|
|
135
|
+
top: var(--vv-top, 0px);
|
|
136
|
+
left: 0;
|
|
137
|
+
right: 0;
|
|
138
|
+
width: 100%;
|
|
139
|
+
}
|
|
75
140
|
html.pwa-standalone,
|
|
76
141
|
html.pwa-standalone body {
|
|
77
142
|
background: oklch(var(--b1));
|
|
@@ -89,11 +154,19 @@ html.pwa-standalone #topbar {
|
|
|
89
154
|
* Inner #composer-row already has small padding; only add safe-area beyond that
|
|
90
155
|
* so we don't stack large empty strips under the input.
|
|
91
156
|
*/
|
|
157
|
+
#chat-form {
|
|
158
|
+
flex-shrink: 0;
|
|
159
|
+
display: flex;
|
|
160
|
+
flex-direction: column;
|
|
161
|
+
}
|
|
92
162
|
#chat-composer {
|
|
93
163
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
|
94
164
|
padding-left: env(safe-area-inset-left, 0px);
|
|
95
165
|
padding-right: env(safe-area-inset-right, 0px);
|
|
96
166
|
}
|
|
167
|
+
html.vv-keyboard #chat-composer {
|
|
168
|
+
padding-bottom: 0;
|
|
169
|
+
}
|
|
97
170
|
/* Welcome / machine picker full-screen panels */
|
|
98
171
|
html.pwa-standalone #welcome,
|
|
99
172
|
html.pwa-standalone #machine-picker {
|
|
@@ -882,28 +955,123 @@ html.pwa-standalone .ma-panel {
|
|
|
882
955
|
.diff-head { color: oklch(var(--bc)/.5); }
|
|
883
956
|
|
|
884
957
|
/* ── Chat composer ───────────────────────────────────────────────────────── */
|
|
885
|
-
/*
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
958
|
+
/* One calm surface holds the prompt and its actions, like a modern messenger. */
|
|
959
|
+
.composer-surface {
|
|
960
|
+
min-height: 0;
|
|
961
|
+
display: flex;
|
|
962
|
+
flex-direction: column;
|
|
963
|
+
overflow: hidden;
|
|
964
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content) 16%, transparent);
|
|
965
|
+
border-radius: 1.35rem;
|
|
966
|
+
background: color-mix(in oklch, var(--color-base-100) 78%, var(--color-base-200));
|
|
967
|
+
box-shadow: 0 8px 24px color-mix(in oklch, black 12%, transparent), inset 0 1px 0 color-mix(in oklch, white 6%, transparent);
|
|
968
|
+
transition: border-color .15s ease, box-shadow .15s ease, background .15s ease;
|
|
969
|
+
}
|
|
970
|
+
.composer-surface:focus-within {
|
|
971
|
+
border-color: color-mix(in oklch, var(--color-primary) 62%, transparent);
|
|
972
|
+
box-shadow: 0 8px 28px color-mix(in oklch, black 15%, transparent), 0 0 0 3px color-mix(in oklch, var(--color-primary) 12%, transparent);
|
|
973
|
+
}
|
|
974
|
+
.composer-actions {
|
|
975
|
+
display: flex;
|
|
976
|
+
align-items: center;
|
|
977
|
+
justify-content: space-between;
|
|
978
|
+
gap: .75rem;
|
|
979
|
+
padding: 0;
|
|
980
|
+
}
|
|
981
|
+
.composer-tools, .composer-submit { display: flex; align-items: center; gap: .35rem; }
|
|
982
|
+
.composer-icon-btn,
|
|
983
|
+
.composer-send-btn,
|
|
984
|
+
.composer-stop-btn {
|
|
985
|
+
border: 0;
|
|
986
|
+
border-radius: 999px;
|
|
987
|
+
cursor: pointer;
|
|
988
|
+
transition: transform .15s ease, background .15s ease, color .15s ease;
|
|
989
|
+
}
|
|
990
|
+
.composer-icon-btn {
|
|
991
|
+
width: 36px;
|
|
992
|
+
height: 36px;
|
|
993
|
+
padding: 0;
|
|
994
|
+
color: color-mix(in oklch, var(--color-base-content) 72%, transparent);
|
|
995
|
+
background: transparent;
|
|
996
|
+
font-size: 17px;
|
|
997
|
+
line-height: 1;
|
|
998
|
+
}
|
|
999
|
+
.composer-icon-btn:hover { background: color-mix(in oklch, var(--color-base-content) 9%, transparent); color: var(--color-base-content); }
|
|
1000
|
+
.composer-send-btn, .composer-stop-btn { height: 36px; min-height: 36px; padding: 0 .95rem; font-size: .8125rem; font-weight: 700; }
|
|
1001
|
+
.composer-send-btn { color: oklch(var(--pc)); background: oklch(var(--p)); box-shadow: 0 3px 10px color-mix(in oklch, oklch(var(--p)) 38%, transparent); }
|
|
1002
|
+
.composer-send-btn:hover { transform: translateY(-1px); background: color-mix(in oklch, oklch(var(--p)) 88%, white); }
|
|
1003
|
+
.composer-send-btn.btn-warning { color: oklch(var(--wc)); background: oklch(var(--wa)); box-shadow: 0 3px 10px color-mix(in oklch, oklch(var(--wa)) 38%, transparent); }
|
|
1004
|
+
.composer-stop-btn { color: oklch(var(--ec)); background: oklch(var(--er)); }
|
|
1005
|
+
.composer-hint { color: color-mix(in oklch, var(--color-base-content) 43%, transparent); font-size: 11px; }
|
|
1006
|
+
.composer-surface:has(.textarea-warning) { border-color: color-mix(in oklch, var(--color-warning) 65%, transparent); }
|
|
1007
|
+
#chat-composer.chat-drop-active {
|
|
1008
|
+
outline: 2px dashed color-mix(in oklch, var(--color-primary) 70%, transparent);
|
|
1009
|
+
outline-offset: -2px;
|
|
1010
|
+
background: color-mix(in oklch, var(--color-primary) 8%, var(--color-base-200));
|
|
1011
|
+
}
|
|
1012
|
+
#chat-attach-bar:not(.hidden) {
|
|
1013
|
+
display: flex;
|
|
1014
|
+
}
|
|
1015
|
+
.chat-thumb {
|
|
1016
|
+
width: 64px;
|
|
1017
|
+
height: 64px;
|
|
1018
|
+
border-radius: 12px;
|
|
1019
|
+
overflow: hidden;
|
|
1020
|
+
border: 1px solid color-mix(in oklch, var(--color-base-content) 12%, transparent);
|
|
1021
|
+
position: relative;
|
|
1022
|
+
flex-shrink: 0;
|
|
1023
|
+
background: color-mix(in oklch, var(--color-base-content) 4%, var(--color-base-100));
|
|
1024
|
+
box-shadow: 0 1px 2px color-mix(in oklch, black 12%, transparent);
|
|
1025
|
+
}
|
|
1026
|
+
.chat-thumb img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
|
1027
|
+
.chat-thumb-path {
|
|
1028
|
+
display: flex;
|
|
1029
|
+
align-items: center;
|
|
1030
|
+
justify-content: center;
|
|
1031
|
+
width: 100%;
|
|
1032
|
+
height: 100%;
|
|
1033
|
+
font-size: 10px;
|
|
1034
|
+
padding: 4px;
|
|
1035
|
+
text-align: center;
|
|
1036
|
+
word-break: break-all;
|
|
1037
|
+
line-height: 1.15;
|
|
1038
|
+
color: color-mix(in oklch, var(--color-base-content) 70%, transparent);
|
|
1039
|
+
background: color-mix(in oklch, var(--color-base-content) 6%, var(--color-base-100));
|
|
1040
|
+
}
|
|
1041
|
+
.chat-thumb-label {
|
|
1042
|
+
display: none;
|
|
1043
|
+
}
|
|
1044
|
+
.chat-thumb-x {
|
|
1045
|
+
position: absolute;
|
|
1046
|
+
top: 4px;
|
|
1047
|
+
right: 4px;
|
|
1048
|
+
background: rgba(255,255,255,.92);
|
|
1049
|
+
color: #334155;
|
|
1050
|
+
border: 1px solid rgba(15,23,42,.1);
|
|
1051
|
+
border-radius: 999px;
|
|
1052
|
+
font-size: 11px;
|
|
1053
|
+
width: 20px;
|
|
1054
|
+
height: 20px;
|
|
1055
|
+
cursor: pointer;
|
|
1056
|
+
line-height: 1;
|
|
1057
|
+
z-index: 1;
|
|
1058
|
+
box-shadow: 0 1px 3px rgba(15,23,42,.18);
|
|
891
1059
|
}
|
|
892
1060
|
#chat-input.chat-input,
|
|
893
1061
|
#chat-input {
|
|
894
1062
|
width: 100%;
|
|
895
|
-
min-height:
|
|
896
|
-
max-height:
|
|
897
|
-
padding: 0
|
|
898
|
-
border
|
|
899
|
-
border:
|
|
900
|
-
background:
|
|
1063
|
+
min-height: 32px;
|
|
1064
|
+
max-height: 72px;
|
|
1065
|
+
padding: 0;
|
|
1066
|
+
border: 0;
|
|
1067
|
+
border-radius: 1.25rem 1.25rem 0 0;
|
|
1068
|
+
background: transparent;
|
|
901
1069
|
color: var(--color-base-content);
|
|
902
1070
|
outline: none !important;
|
|
903
1071
|
box-shadow: none !important;
|
|
904
1072
|
--tw-ring-shadow: 0 0 #0000 !important;
|
|
905
1073
|
--tw-ring-offset-shadow: 0 0 #0000 !important;
|
|
906
|
-
transition:
|
|
1074
|
+
transition: background .15s ease;
|
|
907
1075
|
/* Avoid iOS inherited large text scaling making the box look oversized */
|
|
908
1076
|
-webkit-text-size-adjust: 100%;
|
|
909
1077
|
}
|
|
@@ -974,7 +1142,7 @@ html.pwa-standalone .ma-panel {
|
|
|
974
1142
|
#chat-input:focus-visible {
|
|
975
1143
|
outline: none !important;
|
|
976
1144
|
box-shadow: none !important;
|
|
977
|
-
border-color:
|
|
1145
|
+
border-color: transparent;
|
|
978
1146
|
}
|
|
979
1147
|
#chat-input.textarea-warning {
|
|
980
1148
|
border-color: color-mix(in oklch, var(--color-warning) 65%, transparent);
|
|
@@ -1075,6 +1243,26 @@ html.pwa-standalone .ma-panel {
|
|
|
1075
1243
|
}
|
|
1076
1244
|
.msg-row.user .msg-ts { text-align: right; }
|
|
1077
1245
|
|
|
1246
|
+
.token-bar {
|
|
1247
|
+
display: flex;
|
|
1248
|
+
align-items: flex-start;
|
|
1249
|
+
gap: 0.4rem;
|
|
1250
|
+
padding: 0.1rem 0.45rem;
|
|
1251
|
+
font-size: 10px;
|
|
1252
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
1253
|
+
color: color-mix(in oklch, var(--color-base-content) 38%, transparent);
|
|
1254
|
+
line-height: 1.45;
|
|
1255
|
+
}
|
|
1256
|
+
.token-bar-mark {
|
|
1257
|
+
flex-shrink: 0;
|
|
1258
|
+
opacity: 0.55;
|
|
1259
|
+
}
|
|
1260
|
+
.token-bar-text {
|
|
1261
|
+
min-width: 0;
|
|
1262
|
+
overflow-wrap: anywhere;
|
|
1263
|
+
word-break: break-word;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1078
1266
|
/* ── Long message collapse ───────────────────────────────────────────────── */
|
|
1079
1267
|
.msg-card .msg-body { max-height: 8em; overflow: hidden; mask-image: linear-gradient(to bottom, black 60%, transparent 100%); }
|
|
1080
1268
|
.msg-card.open .msg-body { max-height: none; mask-image: none; }
|
|
@@ -1303,3 +1491,12 @@ html.pwa-standalone .ma-panel {
|
|
|
1303
1491
|
}
|
|
1304
1492
|
.ma-report-md { font-size: 13px; }
|
|
1305
1493
|
.ma-report-item { display: block; }
|
|
1494
|
+
|
|
1495
|
+
@media (max-width: 640px) {
|
|
1496
|
+
.md h1, .md h2, .md h3 {
|
|
1497
|
+
background: none !important;
|
|
1498
|
+
border-bottom: none !important;
|
|
1499
|
+
font-size: 1.02em !important;
|
|
1500
|
+
padding: 0.05em 0 0.15em 0.5em !important;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
package/public/sw.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* Agent UI service worker — cache shell assets only.
|
|
2
2
|
* Never intercepts WebSocket or mutates API semantics beyond offline fallback.
|
|
3
3
|
* Bump CACHE when shipping UI that must invalidate old shells. */
|
|
4
|
-
const CACHE = 'agent-ui-shell-
|
|
4
|
+
const CACHE = 'agent-ui-shell-v9';
|
|
5
5
|
const PRECACHE = [
|
|
6
6
|
'/',
|
|
7
7
|
'/index.html',
|
|
8
8
|
'/manifest.webmanifest',
|
|
9
|
-
'/style.css?v=ui-
|
|
9
|
+
'/style.css?v=ui-10',
|
|
10
10
|
'/style.css',
|
|
11
11
|
'/app.js',
|
|
12
12
|
'/icons/icon-192.png',
|
package/src/app.js
CHANGED
|
@@ -448,13 +448,27 @@ export function createApp() {
|
|
|
448
448
|
const ext = (req.headers['x-filename'] || 'image.png').split('.').pop().replace(/[^a-z0-9]/gi, '') || 'png';
|
|
449
449
|
const name = `claude-upload-${Date.now()}.${ext}`;
|
|
450
450
|
const dest = path.join(os.tmpdir(), name);
|
|
451
|
-
const
|
|
452
|
-
req.
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
451
|
+
const chunks = [];
|
|
452
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
453
|
+
let body = Buffer.concat(chunks);
|
|
454
|
+
// Tolerate mobile share sheets that label Base64 text as a PNG. Only
|
|
455
|
+
// decode when the result has a real image signature.
|
|
456
|
+
const text = body.toString('utf8').trim();
|
|
457
|
+
const m = /^data:image\/[a-z0-9.+-]+;base64,([a-z0-9+/=\s]+)$/i.exec(text);
|
|
458
|
+
const encoded = (m?.[1] || text).replace(/\s/g, '');
|
|
459
|
+
if (/^[a-z0-9+/=]+$/i.test(encoded) && encoded.length % 4 !== 1) {
|
|
460
|
+
try {
|
|
461
|
+
const decoded = Buffer.from(encoded, 'base64');
|
|
462
|
+
const image = decoded.length >= 3 && (
|
|
463
|
+
(decoded[0] === 0x89 && decoded[1] === 0x50 && decoded[2] === 0x4e)
|
|
464
|
+
|| (decoded[0] === 0xff && decoded[1] === 0xd8 && decoded[2] === 0xff)
|
|
465
|
+
|| decoded.subarray(0, 6).toString() === 'GIF87a'
|
|
466
|
+
|| decoded.subarray(8, 12).toString() === 'WEBP'
|
|
467
|
+
);
|
|
468
|
+
if (image) body = decoded;
|
|
469
|
+
} catch { /* preserve the original upload */ }
|
|
470
|
+
}
|
|
471
|
+
await fs.writeFile(dest, body);
|
|
458
472
|
res.json({ path: dest });
|
|
459
473
|
})().catch(e => res.status(500).json({ error: e.message }));
|
|
460
474
|
});
|