claude-simple 0.2.1 → 0.2.3

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/public/shell.js CHANGED
@@ -24,7 +24,10 @@ import { initSettings, openSettings } from './settings.js';
24
24
  import { openMetaAgent, toggleMetaAgent } from './agent-panel.js';
25
25
 
26
26
  import { AuthScreen, AppShell } from './shell/templates.js';
27
- import { updateDashboard, shiftCalMonth, setSelectedCalDate, getDashboardSessions } from './shell/dashboard.js';
27
+ import {
28
+ updateDashboard, shiftCalMonth, setSelectedCalDate, getDashboardSessions,
29
+ openRecentMenu, toggleRecentMenu, closeRecentMenu, positionRecentMenu,
30
+ } from './shell/dashboard.js';
28
31
  import {
29
32
  loadAllSessions, loadWorkspaces, renderSessionList, syncSidebarChrome,
30
33
  refreshModelSelect, refreshEffortSelect, promptCustomModel,
@@ -555,6 +558,7 @@ export function initShell(opts = {}) {
555
558
  });
556
559
  delegate.on('click', '#btn-machine-menu', async e => {
557
560
  e.stopPropagation();
561
+ closeRecentMenu();
558
562
  const menu = $('machine-menu');
559
563
  if (!menu) return;
560
564
  if (menu.classList.contains('hidden')) {
@@ -565,9 +569,44 @@ export function initShell(opts = {}) {
565
569
  menu.classList.add('hidden');
566
570
  }
567
571
  });
572
+ delegate.on('click', '#btn-recent-sessions', e => {
573
+ e.preventDefault();
574
+ e.stopPropagation();
575
+ closeMachineMenu();
576
+ toggleRecentMenu(getDashboardSessions());
577
+ const open = !$('recent-menu')?.classList.contains('hidden');
578
+ const btn = $('btn-recent-sessions');
579
+ if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false');
580
+ });
581
+ delegate.on('click', '#recent-menu-backdrop', e => {
582
+ e.preventDefault();
583
+ e.stopPropagation();
584
+ closeRecentMenu();
585
+ $('btn-recent-sessions')?.setAttribute('aria-expanded', 'false');
586
+ });
568
587
  document.addEventListener('click', e => {
569
- const wrap = $('machine-menu-wrap');
570
- if (wrap && !wrap.contains(e.target)) closeMachineMenu();
588
+ const machineWrap = $('machine-menu-wrap');
589
+ if (machineWrap && !machineWrap.contains(e.target)) closeMachineMenu();
590
+ const menu = $('recent-menu');
591
+ const btn = $('btn-recent-sessions');
592
+ const t = e.target;
593
+ // Menu is portaled (fixed) outside the button wrap — check both
594
+ if (menu && !menu.classList.contains('hidden')) {
595
+ if (menu.contains(t) || btn?.contains(t)) return;
596
+ closeRecentMenu();
597
+ btn?.setAttribute('aria-expanded', 'false');
598
+ }
599
+ });
600
+ // Keep panel glued under topbar on rotate / URL bar show-hide
601
+ window.addEventListener('resize', () => {
602
+ if ($('recent-menu') && !$('recent-menu').classList.contains('hidden')) {
603
+ positionRecentMenu();
604
+ }
605
+ });
606
+ // After picking a session from the topbar menu, close it (row click also resumes).
607
+ delegate.on('click', '#recent-menu [data-session-id]', () => {
608
+ closeRecentMenu();
609
+ $('btn-recent-sessions')?.setAttribute('aria-expanded', 'false');
571
610
  });
572
611
 
573
612
  delegate.on('click', '#btn-edit-project-notes, #project-goal-text, #project-notes-text', () => {
package/public/style.css CHANGED
@@ -11,16 +11,66 @@
11
11
  padding-bottom: env(safe-area-inset-bottom, 0px);
12
12
  }
13
13
  #sidebar.open { transform: translateX(0); box-shadow: 4px 0 20px rgba(0,0,0,.4); }
14
+
15
+ /* Compact chat composer on phones — less height, less dead space under bar */
16
+ #composer-row {
17
+ gap: 0.4rem;
18
+ padding: 0.4rem 0.55rem 0.35rem !important;
19
+ }
20
+ #chat-input.chat-input,
21
+ #chat-input {
22
+ min-height: 36px !important;
23
+ max-height: 96px !important;
24
+ padding: 0.4rem 0.65rem !important;
25
+ border-radius: 0.65rem;
26
+ /* 16px prevents iOS Safari auto-zoom on focus */
27
+ font-size: 16px !important;
28
+ line-height: 1.35 !important;
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;
37
+ }
38
+ #chat-opts {
39
+ padding-top: 0.35rem !important;
40
+ padding-left: 0.55rem !important;
41
+ padding-right: 0.55rem !important;
42
+ gap: 0.35rem !important;
43
+ }
44
+ #scroll-nav {
45
+ bottom: 4.5rem !important;
46
+ }
47
+ /* File list actions always visible on touch (no hover) */
48
+ .file-row-action {
49
+ opacity: 1 !important;
50
+ }
51
+ }
52
+
53
+ /* File row actions: hover-reveal on desktop only */
54
+ @media (min-width: 641px) {
55
+ .file-row-action {
56
+ opacity: 0;
57
+ }
58
+ .group:hover .file-row-action,
59
+ .group:focus-within .file-row-action {
60
+ opacity: 1;
61
+ }
14
62
  }
15
63
 
16
64
  /*
17
- * PWA standalone safe areas:
65
+ * Safe areas (PWA + mobile Safari with viewport-fit=cover):
18
66
  * Do NOT pad the whole body (that leaves a dead empty strip under the chat bar).
19
67
  * Paint chrome into the insets; only inset interactive content.
20
68
  */
21
69
  html, body {
22
70
  height: 100%;
23
71
  height: 100dvh;
72
+ max-height: 100dvh;
73
+ overflow: hidden;
24
74
  }
25
75
  html.pwa-standalone,
26
76
  html.pwa-standalone body {
@@ -34,8 +84,12 @@ html.pwa-standalone #topbar {
34
84
  padding-left: max(0.75rem, env(safe-area-inset-left, 0px));
35
85
  padding-right: max(0.75rem, env(safe-area-inset-right, 0px));
36
86
  }
37
- /* Chat composer: sit above home indicator (background fills the inset) */
38
- html.pwa-standalone #chat-composer {
87
+ /*
88
+ * Chat composer: background fills home-indicator inset.
89
+ * Inner #composer-row already has small padding; only add safe-area beyond that
90
+ * so we don't stack large empty strips under the input.
91
+ */
92
+ #chat-composer {
39
93
  padding-bottom: env(safe-area-inset-bottom, 0px);
40
94
  padding-left: env(safe-area-inset-left, 0px);
41
95
  padding-right: env(safe-area-inset-right, 0px);
@@ -829,6 +883,12 @@ html.pwa-standalone .ma-panel {
829
883
 
830
884
  /* ── Chat composer ───────────────────────────────────────────────────────── */
831
885
  /* Single outer border only — no DaisyUI nested focus ring / inner outline. */
886
+ #composer-row #btn-opts,
887
+ #composer-row #send-btn,
888
+ #composer-row #stop-btn {
889
+ height: 40px;
890
+ min-height: 40px;
891
+ }
832
892
  #chat-input.chat-input,
833
893
  #chat-input {
834
894
  width: 100%;
@@ -844,6 +904,71 @@ html.pwa-standalone .ma-panel {
844
904
  --tw-ring-shadow: 0 0 #0000 !important;
845
905
  --tw-ring-offset-shadow: 0 0 #0000 !important;
846
906
  transition: border-color .15s ease;
907
+ /* Avoid iOS inherited large text scaling making the box look oversized */
908
+ -webkit-text-size-adjust: 100%;
909
+ }
910
+ /* Kill browser password/contact autofill widgets (side checkmarks / arrows) */
911
+ #chat-input::-webkit-contacts-auto-fill-button,
912
+ #chat-input::-webkit-credentials-auto-fill-button,
913
+ #chat-input::-webkit-strong-password-auto-fill-button,
914
+ #ma-input::-webkit-contacts-auto-fill-button,
915
+ #ma-input::-webkit-credentials-auto-fill-button {
916
+ visibility: hidden !important;
917
+ display: none !important;
918
+ pointer-events: none !important;
919
+ position: absolute !important;
920
+ right: 0 !important;
921
+ width: 0 !important;
922
+ height: 0 !important;
923
+ margin: 0 !important;
924
+ padding: 0 !important;
925
+ opacity: 0 !important;
926
+ }
927
+ #chat-input:-webkit-autofill,
928
+ #chat-input:-webkit-autofill:hover,
929
+ #chat-input:-webkit-autofill:focus {
930
+ -webkit-box-shadow: 0 0 0 1000px var(--color-base-100) inset !important;
931
+ transition: background-color 99999s ease-in-out 0s;
932
+ }
933
+
934
+ /* Recent sessions panel (position set in JS under topbar; never use top:100% with fixed) */
935
+ #recent-menu {
936
+ min-width: 16rem;
937
+ overscroll-behavior: contain;
938
+ -webkit-overflow-scrolling: touch;
939
+ }
940
+ #recent-menu.recent-menu-open {
941
+ display: block; /* beat .hidden if both race */
942
+ }
943
+ @media (max-width: 640px) {
944
+ #recent-menu {
945
+ min-width: 0;
946
+ width: auto;
947
+ }
948
+ /* Keep 最近 + hamburger reachable; tuck secondary actions */
949
+ #topbar {
950
+ overflow: visible;
951
+ gap: 0.35rem;
952
+ }
953
+ #topbar .topbar-actions {
954
+ gap: 0.25rem;
955
+ flex-shrink: 0;
956
+ }
957
+ #btn-theme,
958
+ #btn-settings,
959
+ #btn-logout,
960
+ #btn-pwa-install {
961
+ display: none !important;
962
+ }
963
+ #topbar-project {
964
+ font-size: 11px;
965
+ max-width: 30vw;
966
+ }
967
+ #recent-menu-list .recent-menu-item {
968
+ min-height: 44px; /* comfortable touch targets */
969
+ padding-top: 0.5rem;
970
+ padding-bottom: 0.5rem;
971
+ }
847
972
  }
848
973
  #chat-input:focus,
849
974
  #chat-input:focus-visible {
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-v3';
4
+ const CACHE = 'agent-ui-shell-v7';
5
5
  const PRECACHE = [
6
6
  '/',
7
7
  '/index.html',
8
8
  '/manifest.webmanifest',
9
- '/style.css?v=boot-2',
9
+ '/style.css?v=ui-9',
10
10
  '/style.css',
11
11
  '/app.js',
12
12
  '/icons/icon-192.png',
package/src/app.js CHANGED
@@ -383,7 +383,10 @@ function markSessionIdle(sessionId, tempKey) {
383
383
  }
384
384
  }
385
385
 
386
- /** Shrink history payloads: huge tool inputs dominate mobile transfers. */
386
+ /** Max JSON chars kept per tool_use input in history payloads (traffic). */
387
+ const HISTORY_TOOL_INPUT_MAX = 100;
388
+
389
+ /** Shrink history payloads: tool inputs dominate mobile / hub transfers. */
387
390
  function compactHistoryMessages(messages) {
388
391
  if (!Array.isArray(messages)) return messages;
389
392
  return messages.map((m) => {
@@ -392,8 +395,15 @@ function compactHistoryMessages(messages) {
392
395
  let raw;
393
396
  try { raw = typeof m.input === 'string' ? m.input : JSON.stringify(m.input); }
394
397
  catch { raw = String(m.input); }
395
- if (raw.length > 1600) {
396
- return { ...m, input: { _truncated: true, preview: raw.slice(0, 1600), bytes: raw.length } };
398
+ if (raw.length > HISTORY_TOOL_INPUT_MAX) {
399
+ return {
400
+ ...m,
401
+ input: {
402
+ _truncated: true,
403
+ preview: raw.slice(0, HISTORY_TOOL_INPUT_MAX),
404
+ bytes: raw.length,
405
+ },
406
+ };
397
407
  }
398
408
  }
399
409
  if (m.type === 'text' && typeof m.content === 'string' && m.content.length > 48_000) {
@@ -407,8 +417,12 @@ function compactHistoryMessages(messages) {
407
417
  export function createApp() {
408
418
  const app = express();
409
419
 
410
- // Gzip JSON/text API responses when client accepts it (hub tunnel + mobile).
420
+ // Gzip JSON/text API responses when client accepts it (mobile / direct browser).
421
+ // Skip for hub-proxied edge traffic: Node fetch auto-decompresses gzip while
422
+ // still advertising Content-Encoding, which breaks the browser after tunnel
423
+ // re-delivery. Control WS already uses perMessageDeflate.
411
424
  app.use((req, res, next) => {
425
+ if (req.headers['x-hub-token']) return next();
412
426
  const ae = req.headers['accept-encoding'] || '';
413
427
  if (!/\bgzip\b/i.test(ae) || req.method === 'HEAD') return next();
414
428
  const origJson = res.json.bind(res);
@@ -548,8 +562,12 @@ export function createApp() {
548
562
  const stat = await fs.stat(filePath);
549
563
  if (req.query.download === 'true') {
550
564
  const name = path.basename(filePath);
551
- res.setHeader('Content-Disposition', `attachment; filename="${name}"`);
565
+ // ASCII fallback + RFC 5987 so Chinese / spaces filenames download correctly.
566
+ const ascii = name.replace(/[^\x20-\x7E]/g, '_').replace(/["\\]/g, '_');
567
+ const star = encodeURIComponent(name).replace(/['()]/g, c => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
568
+ res.setHeader('Content-Disposition', `attachment; filename="${ascii || 'download'}"; filename*=UTF-8''${star}`);
552
569
  res.setHeader('Content-Type', 'application/octet-stream');
570
+ res.setHeader('Cache-Control', 'no-store');
553
571
  return fsSync.createReadStream(filePath).pipe(res);
554
572
  }
555
573
  // Office / PDF / binary previews need more headroom than plain text
@@ -881,11 +899,11 @@ export function createApp() {
881
899
  const { sessionId } = req.params;
882
900
  if (!/^[0-9a-f-]{36}$/i.test(sessionId)) return res.status(400).json({ error: 'Invalid session id' });
883
901
  const preferred = typeof req.query.agent === 'string' ? req.query.agent : null;
884
- // tail=0 → full history; default last 80 messages (mobile-friendly)
885
- let tail = parseInt(req.query.tail ?? '80', 10);
886
- if (Number.isNaN(tail)) tail = 80;
902
+ // tail=0 → full history (default). Positive = last N messages only.
903
+ let tail = parseInt(req.query.tail ?? '0', 10);
904
+ if (Number.isNaN(tail)) tail = 0;
887
905
  if (tail < 0) tail = 0;
888
- if (tail > 2000) tail = 2000;
906
+ if (tail > 50_000) tail = 50_000;
889
907
  const compact = req.query.compact !== '0' && req.query.compact !== 'false';
890
908
  const opts = {
891
909
  claudeConfigDirs: CLAUDE_CONFIG_DIRS,
@@ -138,9 +138,9 @@ export const UI_EVENT = {
138
138
  /**
139
139
  * Compact tool_use inputs before they hit the wire (mobile tunnel cost).
140
140
  * @param {unknown} input
141
- * @param {number} [max=1200]
141
+ * @param {number} [max=100]
142
142
  */
143
- export function compactToolInput(input, max = 1200) {
143
+ export function compactToolInput(input, max = 100) {
144
144
  if (input == null) return input;
145
145
  let raw;
146
146
  try {