@linxin666/dsh-remote-web-ui 0.2.0 → 0.2.2
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.i18n.yaml +2 -2
- package/README.md +17 -8
- package/README.zh.md +5 -5
- package/lib/client.js +259 -43
- package/lib/client.js.map +1 -1
- package/lib/index.js +26 -7
- package/lib/mobile.js +209 -39
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +22 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +142 -3
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +11 -0
- package/lib/types/client/RemoteSettingsCard.d.ts +2 -2
- package/lib/types/client/UpdateEntry.d.ts.map +1 -1
- package/lib/types/client/UpdateEntry.js +8 -5
- package/lib/types/client/UpdatePanel.d.ts +3 -1
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +3 -3
- package/lib/types/client/locales.d.ts +4 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +8 -4
- package/lib/types/client/pair-api.d.ts +1 -6
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +1 -10
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +2 -0
- package/lib/types/mobile/api.d.ts +10 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +4 -0
- package/lib/types/mobile/messages.d.ts +24 -0
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +60 -23
- package/lib/types/mobile/messages.test.js +49 -1
- package/lib/types/mobile/mobile-styles.d.ts +1 -1
- package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
- package/lib/types/mobile/mobile-styles.js +38 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +34 -11
- package/lib/types/mobile/views/SessionListView.d.ts.map +1 -1
- package/lib/types/mobile/views/SessionListView.js +30 -4
- package/lib/types/mobile/views/SessionListView.test.js +58 -1
- package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
- package/lib/types/mobile/views/WorkspaceView.js +4 -2
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +10 -1
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +22 -2
- package/lib/types/tunnel.d.ts +1 -0
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +7 -2
- package/lib/types/update.d.ts +1 -1
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +1 -1
- package/package.json +1 -2
- package/src/client/PluginSettingsCard.tsx +219 -18
- package/src/client/RemoteEntry.tsx +10 -0
- package/src/client/RemoteSettingsCard.tsx +2 -2
- package/src/client/UpdateEntry.tsx +9 -5
- package/src/client/UpdatePanel.tsx +12 -1
- package/src/client/locales.ts +8 -4
- package/src/client/pair-api.ts +1 -10
- package/src/client/settings-card.module.css +90 -1
- package/src/gate.ts +2 -0
- package/src/mobile/api.ts +14 -1
- package/src/mobile/messages.test.ts +52 -1
- package/src/mobile/messages.ts +65 -22
- package/src/mobile/mobile-styles.ts +38 -0
- package/src/mobile/mobile-theme.test.ts +1 -1
- package/src/mobile/views/ChatView.tsx +33 -10
- package/src/mobile/views/SessionListView.test.tsx +69 -1
- package/src/mobile/views/SessionListView.tsx +58 -5
- package/src/mobile/views/WorkspaceView.tsx +4 -2
- package/src/mobile-api.ts +9 -1
- package/src/routes.ts +22 -2
- package/src/tunnel.ts +7 -2
- package/src/update.ts +2 -2
|
@@ -83,17 +83,6 @@ function chunkTarget(data) {
|
|
|
83
83
|
result.step = step;
|
|
84
84
|
return result;
|
|
85
85
|
}
|
|
86
|
-
/** Max message seq across an existing list, or -1 for an empty list. */
|
|
87
|
-
function watermarkOf(existing) {
|
|
88
|
-
if (existing === undefined)
|
|
89
|
-
return -1;
|
|
90
|
-
let maxSeq = -1;
|
|
91
|
-
for (const message of existing) {
|
|
92
|
-
if (message.seq > maxSeq)
|
|
93
|
-
maxSeq = message.seq;
|
|
94
|
-
}
|
|
95
|
-
return maxSeq;
|
|
96
|
-
}
|
|
97
86
|
function createState(existing) {
|
|
98
87
|
const messages = existing === undefined ? [] : [...existing];
|
|
99
88
|
const state = {
|
|
@@ -103,8 +92,11 @@ function createState(existing) {
|
|
|
103
92
|
turnStepMessage: new Map(),
|
|
104
93
|
messageTurn: new Map(),
|
|
105
94
|
toolNames: new Map(),
|
|
95
|
+
maxSeq: -1,
|
|
106
96
|
};
|
|
107
97
|
for (const message of messages) {
|
|
98
|
+
if (message.seq > state.maxSeq)
|
|
99
|
+
state.maxSeq = message.seq;
|
|
108
100
|
state.byId.set(message.id, message);
|
|
109
101
|
if (message.kind !== 'assistant')
|
|
110
102
|
continue;
|
|
@@ -167,6 +159,8 @@ function retargetTurnStep(state, key, oldMessage, next) {
|
|
|
167
159
|
}
|
|
168
160
|
/** Fold one event into the working state. Assumes the event passes the watermark. */
|
|
169
161
|
function applyEvent(state, event) {
|
|
162
|
+
if (event.seq > state.maxSeq)
|
|
163
|
+
state.maxSeq = event.seq;
|
|
170
164
|
switch (event.type) {
|
|
171
165
|
case 'user/message':
|
|
172
166
|
applyUserMessage(state, event);
|
|
@@ -489,18 +483,61 @@ function applyTurnEnd(state, event) {
|
|
|
489
483
|
* @returns messages sorted by seq.
|
|
490
484
|
*/
|
|
491
485
|
export function foldEvents(events, existing) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
486
|
+
return new EventFolder(existing).fold(events);
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Incremental folder for one message stream. Live chat folds one event at a
|
|
490
|
+
* time; rebuilding the five index maps by scanning every message per event
|
|
491
|
+
* made that path O(n) per event (O(n * events) per turn). A folder keeps the
|
|
492
|
+
* indexes alive across folds, applies each event in O(1) map operations, and
|
|
493
|
+
* returns the previous snapshot identity unchanged when nothing applied, so
|
|
494
|
+
* React skips the re-render entirely. Replayed events are no-ops: the maxSeq
|
|
495
|
+
* watermark advanced by the first application skips them, which also makes a
|
|
496
|
+
* double-invoked React state updater harmless.
|
|
497
|
+
*/
|
|
498
|
+
export class EventFolder {
|
|
499
|
+
state;
|
|
500
|
+
snapshotList;
|
|
501
|
+
/** @param initial - seed rows (history tail load); omit for an empty stream. */
|
|
502
|
+
constructor(initial) {
|
|
503
|
+
this.state = createState(initial);
|
|
499
504
|
}
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
505
|
+
/** Fold one batch incrementally; returns the current snapshot list. */
|
|
506
|
+
fold(events) {
|
|
507
|
+
const sorted = [...events].sort((a, b) => a.seq - b.seq);
|
|
508
|
+
let applied = false;
|
|
509
|
+
for (const event of sorted) {
|
|
510
|
+
if (event.seq <= this.state.maxSeq)
|
|
511
|
+
continue;
|
|
512
|
+
applyEvent(this.state, event);
|
|
513
|
+
applied = true;
|
|
514
|
+
}
|
|
515
|
+
if (!applied && this.snapshotList !== undefined)
|
|
516
|
+
return this.snapshotList;
|
|
517
|
+
this.snapshotList = snapshotOf(this.state);
|
|
518
|
+
return this.snapshotList;
|
|
519
|
+
}
|
|
520
|
+
/** Replace the whole stream (history reload / session switch). */
|
|
521
|
+
seed(messages) {
|
|
522
|
+
this.state = createState(messages);
|
|
523
|
+
this.snapshotList = undefined;
|
|
524
|
+
}
|
|
525
|
+
/** Prepend an older history page (exact seam; no overlapping seqs). */
|
|
526
|
+
prepend(older) {
|
|
527
|
+
this.state = createState([...older, ...this.state.messages]);
|
|
528
|
+
this.snapshotList = undefined;
|
|
529
|
+
}
|
|
530
|
+
/** Current snapshot list; a fresh copy whenever the folder changed. */
|
|
531
|
+
snapshot() {
|
|
532
|
+
if (this.snapshotList !== undefined)
|
|
533
|
+
return this.snapshotList;
|
|
534
|
+
this.snapshotList = snapshotOf(this.state);
|
|
535
|
+
return this.snapshotList;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
/** Copy the folder's rows and keep them seq-ordered (skips re-sorting the common ordered case). */
|
|
539
|
+
function snapshotOf(state) {
|
|
540
|
+
const out = [...state.messages];
|
|
504
541
|
let ordered = true;
|
|
505
542
|
for (let index = 1; index < out.length; index += 1) {
|
|
506
543
|
const prev = out[index - 1];
|
|
@@ -510,5 +547,5 @@ export function foldEvents(events, existing) {
|
|
|
510
547
|
break;
|
|
511
548
|
}
|
|
512
549
|
}
|
|
513
|
-
return ordered ? out :
|
|
550
|
+
return ordered ? out : out.sort((a, b) => a.seq - b.seq || (a.id < b.id ? -1 : 1));
|
|
514
551
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** foldEvents: message-list folding from a session event stream. */
|
|
2
2
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { foldEvents } from './messages.js';
|
|
3
|
+
import { EventFolder, foldEvents } from './messages.js';
|
|
4
4
|
/** Assemble one event with an auto-incrementing seq / time. */
|
|
5
5
|
function makeEvent(type, data, seq, time = seq * 1_000) {
|
|
6
6
|
return { type, seq, time, data };
|
|
@@ -286,3 +286,51 @@ describe('foldEvents', () => {
|
|
|
286
286
|
expect(result[0]).toMatchObject({ id: 'u-1', kind: 'user' });
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
|
+
describe('EventFolder incremental folding', () => {
|
|
290
|
+
it('matches a one-shot fold when events arrive one at a time', () => {
|
|
291
|
+
const stream = [
|
|
292
|
+
makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
|
|
293
|
+
textChunk(0, 0, '你', 1),
|
|
294
|
+
textChunk(0, 0, '好', 2),
|
|
295
|
+
makeEvent('assistant/message', assistantMessageData('a-1', 0, 0, '你好'), 3),
|
|
296
|
+
makeEvent('message/update', { id: 'u-1', text: '更新' }, 4),
|
|
297
|
+
];
|
|
298
|
+
const oneShot = foldEvents(stream);
|
|
299
|
+
const folder = new EventFolder();
|
|
300
|
+
let incremental = [];
|
|
301
|
+
for (const event of stream)
|
|
302
|
+
incremental = folder.fold([event]);
|
|
303
|
+
expect(incremental).toEqual(oneShot);
|
|
304
|
+
});
|
|
305
|
+
it('replays and re-folds are no-ops and reuse the previous snapshot identity', () => {
|
|
306
|
+
const folder = new EventFolder(foldEvents([
|
|
307
|
+
makeEvent('user/message', userMessageData('u-1', 'hi'), 0),
|
|
308
|
+
textChunk(0, 0, '你', 1),
|
|
309
|
+
]));
|
|
310
|
+
const first = folder.fold([textChunk(0, 0, '好', 2)]);
|
|
311
|
+
expect(first[first.length - 1]).toMatchObject({ text: '你好', pending: true });
|
|
312
|
+
// Same event again (wire replay or a double-invoked state updater): no change, same identity.
|
|
313
|
+
const replay = folder.fold([textChunk(0, 0, '好', 2)]);
|
|
314
|
+
expect(replay).toBe(first);
|
|
315
|
+
// A no-op batch over an already folded stream also keeps the identity.
|
|
316
|
+
expect(folder.fold([])).toBe(first);
|
|
317
|
+
});
|
|
318
|
+
it('prepends older pages and keeps folding live events on top', () => {
|
|
319
|
+
const folder = new EventFolder(foldEvents([
|
|
320
|
+
makeEvent('user/message', userMessageData('u-2', '第二页'), 10),
|
|
321
|
+
]));
|
|
322
|
+
folder.prepend(foldEvents([makeEvent('user/message', userMessageData('u-1', '第一页'), 5)]));
|
|
323
|
+
const withLive = folder.fold([textChunk(0, 0, '新', 11)]);
|
|
324
|
+
expect(withLive.map(message => message.id)).toEqual(['u-1', 'u-2', 'assistant,0.0#11']);
|
|
325
|
+
// The live fold must not lose the prepended rows on later events.
|
|
326
|
+
const later = folder.fold([textChunk(0, 0, '续', 12)]);
|
|
327
|
+
expect(later.map(message => message.id)).toEqual(['u-1', 'u-2', 'assistant,0.0#11']);
|
|
328
|
+
expect(later[2]).toMatchObject({ text: '新续', pending: true });
|
|
329
|
+
});
|
|
330
|
+
it('seed replaces the whole stream', () => {
|
|
331
|
+
const folder = new EventFolder(foldEvents([makeEvent('user/message', userMessageData('u-1', '旧'), 0)]));
|
|
332
|
+
folder.seed(foldEvents([makeEvent('user/message', userMessageData('u-2', '新'), 5)]));
|
|
333
|
+
expect(folder.snapshot().map(message => message.id)).toEqual(['u-2']);
|
|
334
|
+
expect(folder.fold([textChunk(0, 0, '追加', 6)]).map(message => message.id)).toEqual(['u-2', 'assistant,0.0#6']);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* Mobile surface stylesheet, shipped as a string and injected at boot (the
|
|
3
3
|
* standalone page has no CSS-module pipeline).
|
|
4
4
|
*/
|
|
5
|
-
export declare const mobileCss = "/* Mobile surface chrome: standalone stylesheet (no main-UI design tokens \u2014\n this page boots without the shell). Light-first palette; the dark palette\n applies only when the header toggle sets [data-theme='dark']. */\n\n:root {\n --m-bg: #f3f5f9;\n --m-bg-raised: #ffffff;\n --m-bg-input: #ffffff;\n --m-border: #dfe4ec;\n --m-text: #1c2333;\n --m-text-secondary: #4d5768;\n --m-text-tertiary: #8b95a7;\n --m-accent: #2f6fed;\n --m-accent-soft: rgba(47, 111, 237, 0.12);\n --m-danger: #d64545;\n --m-success: #1e9e6a;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);\n --m-shadow-raise: 0 6px 20px rgba(16, 24, 40, 0.1);\n --m-backdrop: rgba(15, 23, 42, 0.4);\n}\n\n:root[data-theme='dark'] {\n --m-bg: #111418;\n --m-bg-raised: #1a1f26;\n --m-bg-input: #20262e;\n --m-border: #2b333d;\n --m-text: #e8eaed;\n --m-text-secondary: #9aa3ad;\n --m-text-tertiary: #6b747f;\n --m-accent: #4f8cff;\n --m-accent-soft: rgba(79, 140, 255, 0.14);\n --m-danger: #e5534b;\n --m-success: #3fb68b;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n --m-shadow-raise: 0 6px 20px rgba(0, 0, 0, 0.4);\n --m-backdrop: rgba(0, 0, 0, 0.55);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n height: 100%;\n background: var(--m-bg);\n color: var(--m-text);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Noto Sans SC', 'Segoe UI', sans-serif;\n font-size: 15px;\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n}\n\n/* The app fills exactly one viewport: the page itself never scrolls, each\n view owns its scroll region (chat history, session lists) and the chat\n composer stays pinned to the bottom of the screen. */\n#root {\n height: 100dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.mobile {\n min-height: 0;\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n/* \u2500\u2500 header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-header {\n position: sticky;\n top: 0;\n z-index: 10;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: calc(env(safe-area-inset-top, 0px) + 12px) 16px 10px;\n background: color-mix(in srgb, var(--m-bg) 88%, transparent);\n backdrop-filter: blur(10px);\n border-bottom: 1px solid var(--m-border);\n}\n\n.mobile-title {\n margin: 0;\n font-size: 17px;\n font-weight: 600;\n}\n\n.mobile-titleInline {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mobile-back {\n flex: none;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text);\n font-size: 22px;\n line-height: 1;\n cursor: pointer;\n transition: background-color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-back:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-back:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle {\n flex: none;\n margin-left: auto;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text-secondary);\n cursor: pointer;\n transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-theme-toggle:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-theme-toggle:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle svg {\n width: 18px;\n height: 18px;\n}\n\n/* \u2500\u2500 lists \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n list-style: none;\n margin: 0;\n padding: 8px 0 calc(env(safe-area-inset-bottom, 0px) + 16px);\n}\n\n.mobile-row {\n display: flex;\n align-items: center;\n gap: 10px;\n width: calc(100% - 24px);\n margin: 0 12px 8px;\n padding: 13px 14px;\n border: 1px solid var(--m-border);\n border-radius: 14px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.mobile-row:active {\n background: color-mix(in srgb, var(--m-text) 5%, var(--m-bg-raised));\n}\n\n.mobile-rowMain {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n\n.mobile-rowTitle {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 15px;\n}\n\n.mobile-rowMeta {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.mobile-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 18px;\n}\n\n.mobile-live {\n margin-left: 6px;\n color: var(--m-success);\n font-size: 10px;\n vertical-align: middle;\n}\n\n/* \u2500\u2500 empty / error states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px;\n}\n\n.mobile-muted {\n margin: 0;\n color: var(--m-text-tertiary);\n}\n\n.mobile-error {\n margin: 0;\n color: var(--m-danger);\n}\n\n.mobile-pad {\n padding: 8px 16px;\n}\n\n/* \u2500\u2500 buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-button {\n height: 38px;\n padding: 0 18px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-raised);\n color: var(--m-text);\n font-size: 14px;\n cursor: pointer;\n}\n\n.mobile-button:disabled {\n opacity: 0.5;\n}\n\n.mobile-block {\n display: block;\n width: calc(100% - 32px);\n margin: 4px 16px;\n}\n\n/* Primary action (create session): accent fill, readable on both themes. */\n.mobile-new {\n display: block;\n width: 100%;\n height: 44px;\n border: none;\n border-radius: 12px;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(47, 111, 237, 0.3);\n}\n\n.mobile-new:active {\n opacity: 0.88;\n}\n\n.mobile-new:disabled {\n opacity: 0.6;\n}\n\n.mobile-hint {\n display: block;\n margin-top: 2px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n/* \u2500\u2500 chat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.chat-scroll {\n flex: 1;\n overflow-y: auto;\n padding: 12px 14px calc(env(safe-area-inset-bottom, 0px) + 12px);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.chat-msg {\n max-width: 88%;\n padding: 10px 12px;\n border-radius: var(--m-radius);\n font-size: 14.5px;\n line-height: 1.55;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.chat-msg-user {\n align-self: flex-end;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n border-bottom-right-radius: 4px;\n box-shadow: 0 1px 3px rgba(47, 111, 237, 0.28);\n}\n\n.chat-msg-assistant {\n align-self: flex-start;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom-left-radius: 4px;\n box-shadow: var(--m-shadow);\n}\n\n.chat-msg-pending .chat-msg-text::after {\n content: '\u258D';\n animation: chat-blink 1s steps(2) infinite;\n}\n\n@keyframes chat-blink {\n 50% {\n opacity: 0;\n }\n}\n\n.chat-msg-failed {\n border-color: var(--m-danger);\n}\n\n.chat-msg-failtag {\n display: inline-block;\n margin-top: 6px;\n padding: 1px 8px;\n border: 1px solid var(--m-danger);\n border-radius: 999px;\n color: var(--m-danger);\n font-size: 11px;\n}\n\n.chat-msg-time {\n display: block;\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 10.5px;\n line-height: 1.2;\n}\n\n.chat-msg-toggle {\n display: block;\n margin-top: 8px;\n padding: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-accent);\n font-size: 13px;\n cursor: pointer;\n}\n\n/* Assistant markdown content (GFM subset; renderer in markdown.ts). */\n.chat-md {\n white-space: normal;\n}\n.chat-md-collapsed {\n max-height: 45vh;\n overflow: hidden;\n position: relative;\n}\n.chat-md-collapsed::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 28px;\n background: linear-gradient(transparent, var(--m-bg-raised));\n pointer-events: none;\n}\n.chat-md-body p { margin: 0 0 8px; }\n.chat-md-body p:last-child { margin-bottom: 0; }\n.chat-md-body h1, .chat-md-body h2, .chat-md-body h3,\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 {\n margin: 12px 0 6px;\n font-weight: 650;\n line-height: 1.3;\n}\n.chat-md-body h1 { font-size: 1.35em; }\n.chat-md-body h2 { font-size: 1.25em; }\n.chat-md-body h3 { font-size: 1.15em; }\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 { font-size: 1.05em; }\n.chat-md-body code {\n font-family: var(--m-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n font-size: 0.9em;\n background: var(--m-accent-soft);\n padding: 1px 5px;\n border-radius: 4px;\n}\n.chat-md-body pre {\n margin: 8px 0;\n padding: 10px;\n background: #101726;\n color: #dbe4f5;\n border-radius: 8px;\n overflow-x: auto;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.chat-md-body pre code {\n background: transparent;\n padding: 0;\n color: inherit;\n}\n.chat-md-body ul, .chat-md-body ol { margin: 4px 0 8px; padding-left: 22px; }\n.chat-md-body li { margin: 2px 0; }\n.chat-md-body blockquote {\n margin: 8px 0;\n padding: 4px 10px;\n border-left: 3px solid var(--m-accent);\n background: var(--m-accent-soft);\n border-radius: 0 6px 6px 0;\n color: var(--m-text-secondary);\n}\n.chat-md-body table {\n margin: 8px 0;\n border-collapse: collapse;\n display: block;\n overflow-x: auto;\n font-size: 13px;\n}\n.chat-md-body th, .chat-md-body td {\n border: 1px solid var(--m-border);\n padding: 4px 8px;\n}\n.chat-md-body th { background: var(--m-accent-soft); }\n.chat-md-body a { color: var(--m-accent); }\n.chat-md-body hr { border: none; border-top: 1px solid var(--m-border); margin: 10px 0; }\n.chat-md-body img { max-width: 100%; border-radius: 8px; }\n\n/* \u2500\u2500 message disclosures (reasoning / tools) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-disclosure {\n margin-bottom: 8px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: color-mix(in srgb, var(--m-bg) 55%, transparent);\n overflow: hidden;\n}\n\n.chat-disclosure-head {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 7px 10px;\n border: none;\n background: transparent;\n color: var(--m-text);\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.chat-disclosure-caret {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n line-height: 1;\n transition: transform 0.15s ease;\n}\n\n.chat-disclosure-open .chat-disclosure-caret {\n transform: rotate(90deg);\n}\n\n.chat-disclosure-label {\n flex: none;\n color: var(--m-text-secondary);\n}\n\n.chat-reasoning[data-pending] .chat-disclosure-label {\n color: var(--m-accent);\n}\n\n.chat-disclosure-summary {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.chat-disclosure-count {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-disclosure-body {\n padding: 8px 10px 10px;\n border-top: 1px dashed var(--m-border);\n color: var(--m-text-secondary);\n font-size: 12.5px;\n line-height: 1.6;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n max-height: 40vh;\n overflow-y: auto;\n}\n\n.chat-tools-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.chat-tool-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.chat-tool-name {\n color: var(--m-text);\n font-size: 12.5px;\n font-weight: 600;\n}\n\n.chat-tool-args {\n margin: 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--m-bg-input);\n color: var(--m-text-secondary);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n/* \u2500\u2500 composer toolbar (model / permission chips) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-tools {\n display: flex;\n gap: 8px;\n padding: 6px 12px 0;\n}\n\n.chat-chip {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n font-size: 12px;\n cursor: pointer;\n}\n\n.chat-chip:active {\n border-color: var(--m-accent);\n background: var(--m-accent-soft);\n}\n\n.chat-chip-label {\n flex: none;\n color: var(--m-text-tertiary);\n}\n\n.chat-chip-value {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.chat-chip-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n}\n\n/* \u2500\u2500 bottom sheets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-backdrop {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n background: var(--m-backdrop);\n animation: sheet-fade 0.18s ease;\n}\n\n@keyframes sheet-fade {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.sheet {\n max-height: 74dvh;\n display: flex;\n flex-direction: column;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom: none;\n border-radius: 16px 16px 0 0;\n box-shadow: var(--m-shadow-raise);\n padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 10px);\n animation: sheet-up 0.22s ease;\n}\n\n@keyframes sheet-up {\n from {\n transform: translateY(40px);\n opacity: 0.6;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n.sheet-handle {\n flex: none;\n align-self: center;\n width: 36px;\n height: 4px;\n margin: 8px 0 4px;\n border-radius: 999px;\n background: var(--m-border);\n}\n\n.sheet-title {\n flex: none;\n padding: 6px 16px 10px;\n font-size: 15px;\n font-weight: 600;\n}\n\n.sheet-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 0 12px 8px;\n}\n\n.sheet-status {\n padding: 18px 8px;\n color: var(--m-text-tertiary);\n font-size: 13px;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n}\n\n.sheet-status-error {\n color: var(--m-danger);\n}\n\n.sheet-hint {\n margin: 0;\n color: var(--m-text-secondary);\n font-size: 12px;\n line-height: 1.5;\n max-width: 32em;\n}\n\n.sheet-error {\n margin: 0 0 8px;\n color: var(--m-danger);\n font-size: 12.5px;\n}\n\n.sheet-section {\n margin-bottom: 14px;\n}\n\n.sheet-section-title {\n padding: 4px 8px 6px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n font-weight: 600;\n}\n\n.sheet-option {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 10px 12px;\n border: 1px solid transparent;\n border-radius: 10px;\n background: transparent;\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.sheet-option:active {\n background: var(--m-bg-input);\n}\n\n.sheet-option-selected {\n border-color: var(--m-accent);\n background: color-mix(in srgb, var(--m-accent) 12%, transparent);\n}\n\n.sheet-option:disabled {\n opacity: 0.5;\n}\n\n.sheet-option-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-option-title {\n font-size: 14px;\n}\n\n.sheet-option-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-option-check {\n flex: none;\n color: var(--m-accent);\n font-size: 15px;\n font-weight: 700;\n}\n\n.sheet-confirm-desc {\n margin: 0 4px 14px;\n color: var(--m-text-secondary);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.sheet-confirm-actions {\n display: flex;\n gap: 10px;\n padding: 4px;\n}\n\n.sheet-confirm-actions .mobile-button {\n flex: 1;\n}\n\n.sheet-confirm-danger {\n flex: 1;\n height: 38px;\n border: none;\n border-radius: 10px;\n background: var(--m-danger);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.sheet-confirm-danger:disabled {\n opacity: 0.5;\n}\n\n.chat-meta {\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-typing {\n color: var(--m-text-tertiary);\n font-size: 13px;\n padding: 4px 2px;\n}\n\n.chat-inputbar {\n display: flex;\n gap: 8px;\n padding: 10px 12px calc(env(safe-area-inset-bottom, 0px) + 10px);\n border-top: 1px solid var(--m-border);\n background: var(--m-bg);\n}\n\n.chat-input {\n flex: 1;\n min-width: 0;\n min-height: 40px;\n max-height: 120px;\n /* Grow with the content (still bounded by min/max-height); browsers\n without field-sizing support ignore this line and keep the old size. */\n field-sizing: content;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n font-size: 14.5px;\n resize: none;\n outline: none;\n}\n\n.chat-input:focus {\n border-color: var(--m-accent);\n}\n\n.chat-send {\n flex: none;\n align-self: flex-end;\n height: 40px;\n padding: 0 16px;\n border: none;\n border-radius: 10px;\n background: var(--m-accent);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.chat-send:disabled {\n opacity: 0.5;\n}\n\n.chat-load-older {\n align-self: center;\n margin: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-text-secondary);\n font-size: 13px;\n cursor: pointer;\n padding: 6px 10px;\n}\n\n/* Touch surfaces have no hover, so keyboard focus must be fully visible.\n One shared ring keeps every interactive control consistent and readable\n on both palettes. */\n.mobile-row:focus-visible,\n.mobile-button:focus-visible,\n.mobile-new:focus-visible,\n.chat-send:focus-visible,\n.chat-chip:focus-visible,\n.chat-msg-toggle:focus-visible,\n.chat-load-older:focus-visible,\n.chat-disclosure-head:focus-visible,\n.sheet-option:focus-visible,\n.sheet-confirm-danger:focus-visible,\n.sheet-toggle-switch:focus-visible,\n.chat-input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n/* \u2500\u2500 chat context usage chip (chat-tools row) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-context {\n flex: none;\n align-self: center;\n padding: 2px 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: color-mix(in srgb, var(--m-text-tertiary) 10%, transparent);\n color: var(--m-text-tertiary);\n font-size: 11px;\n line-height: 1.6;\n white-space: nowrap;\n}\n\n.chat-context-warn {\n border-color: var(--m-danger);\n background: color-mix(in srgb, var(--m-danger) 14%, transparent);\n color: var(--m-danger);\n}\n\n/* \u2500\u2500 display sheet toggle rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-toggle-row {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 8px;\n border-bottom: 1px solid var(--m-border);\n}\n\n.sheet-toggle-row:last-child {\n border-bottom: none;\n}\n\n.sheet-toggle-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-toggle-title {\n font-size: 14px;\n}\n\n.sheet-toggle-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-toggle-switch {\n position: relative;\n flex: none;\n width: 44px;\n height: 26px;\n border: none;\n border-radius: 999px;\n background: var(--m-border);\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n\n.sheet-toggle-switch-on {\n background: var(--m-accent);\n}\n\n.sheet-toggle-switch-knob {\n position: absolute;\n top: 3px;\n left: 3px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 1px 2px rgba(16, 24, 40, 0.2);\n transition: transform 0.15s ease;\n}\n\n.sheet-toggle-switch-on .sheet-toggle-switch-knob {\n transform: translateX(18px);\n}\n\n/* Accessibility: collapse every interaction/enter animation for\n reduced-motion users; surface layout itself stays static. */\n@media (prefers-reduced-motion: reduce) {\n .mobile-theme-toggle,\n .mobile-back,\n .mobile-row,\n .mobile-button,\n .mobile-new,\n .chat-send,\n .chat-chip,\n .chat-msg-toggle,\n .chat-load-older,\n .chat-disclosure-head,\n .chat-disclosure-caret,\n .chat-msg-pending .chat-msg-text::after,\n .sheet-backdrop,\n .sheet,\n .sheet-option,\n .sheet-confirm-danger,\n .sheet-toggle-switch,\n .sheet-toggle-switch-knob,\n .chat-input {\n animation: none;\n transition: none;\n }\n}\n\n/* \u2500\u2500 installed-app pairing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-pair {\n min-height: 100dvh;\n display: grid;\n place-items: center;\n padding: 24px;\n}\n\n.mobile-pairCard {\n display: grid;\n width: min(100%, 420px);\n gap: 12px;\n padding: 24px;\n border: 1px solid var(--m-border);\n border-radius: 12px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow-raise);\n}\n\n.mobile-pairLabel {\n color: var(--m-text-secondary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.mobile-pairInput {\n width: 100%;\n min-height: 42px;\n box-sizing: border-box;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n outline: none;\n}\n\n.mobile-pairInput:focus-visible {\n border-color: var(--m-accent);\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-pairSubmit {\n width: 100%;\n}\n";
|
|
5
|
+
export declare const mobileCss = "/* Mobile surface chrome: standalone stylesheet (no main-UI design tokens \u2014\n this page boots without the shell). Light-first palette; the dark palette\n applies only when the header toggle sets [data-theme='dark']. */\n\n:root {\n --m-bg: #f3f5f9;\n --m-bg-raised: #ffffff;\n --m-bg-input: #ffffff;\n --m-border: #dfe4ec;\n --m-text: #1c2333;\n --m-text-secondary: #4d5768;\n --m-text-tertiary: #8b95a7;\n --m-accent: #2f6fed;\n --m-accent-soft: rgba(47, 111, 237, 0.12);\n --m-danger: #d64545;\n --m-success: #1e9e6a;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(16, 24, 40, 0.06);\n --m-shadow-raise: 0 6px 20px rgba(16, 24, 40, 0.1);\n --m-backdrop: rgba(15, 23, 42, 0.4);\n}\n\n:root[data-theme='dark'] {\n --m-bg: #111418;\n --m-bg-raised: #1a1f26;\n --m-bg-input: #20262e;\n --m-border: #2b333d;\n --m-text: #e8eaed;\n --m-text-secondary: #9aa3ad;\n --m-text-tertiary: #6b747f;\n --m-accent: #4f8cff;\n --m-accent-soft: rgba(79, 140, 255, 0.14);\n --m-danger: #e5534b;\n --m-success: #3fb68b;\n --m-radius: 12px;\n --m-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n --m-shadow-raise: 0 6px 20px rgba(0, 0, 0, 0.4);\n --m-backdrop: rgba(0, 0, 0, 0.55);\n}\n\n* {\n box-sizing: border-box;\n}\n\nhtml,\nbody {\n margin: 0;\n padding: 0;\n height: 100%;\n background: var(--m-bg);\n color: var(--m-text);\n font-family: -apple-system, BlinkMacSystemFont, 'PingFang SC', 'Noto Sans SC', 'Segoe UI', sans-serif;\n font-size: 15px;\n line-height: 1.5;\n -webkit-text-size-adjust: 100%;\n}\n\n/* The app fills exactly one viewport: the page itself never scrolls, each\n view owns its scroll region (chat history, session lists) and the chat\n composer stays pinned to the bottom of the screen. */\n#root {\n height: 100dvh;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n.mobile {\n min-height: 0;\n flex: 1;\n display: flex;\n flex-direction: column;\n overflow: hidden;\n}\n\n/* \u2500\u2500 header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-header {\n position: sticky;\n top: 0;\n z-index: 10;\n flex-shrink: 0;\n display: flex;\n align-items: center;\n gap: 8px;\n padding: calc(env(safe-area-inset-top, 0px) + 12px) 16px 10px;\n background: color-mix(in srgb, var(--m-bg) 88%, transparent);\n backdrop-filter: blur(10px);\n border-bottom: 1px solid var(--m-border);\n}\n\n.mobile-title {\n margin: 0;\n font-size: 17px;\n font-weight: 600;\n}\n\n.mobile-titleInline {\n flex: 1;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.mobile-back {\n flex: none;\n width: 32px;\n height: 32px;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text);\n font-size: 22px;\n line-height: 1;\n cursor: pointer;\n transition: background-color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-back:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-back:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle {\n flex: none;\n margin-left: auto;\n width: 32px;\n height: 32px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border: none;\n border-radius: 50%;\n background: transparent;\n color: var(--m-text-secondary);\n cursor: pointer;\n transition: background-color 0.12s ease, color 0.12s ease, box-shadow 0.12s ease;\n}\n\n.mobile-theme-toggle:active {\n background: var(--m-bg-raised);\n}\n\n.mobile-theme-toggle:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-theme-toggle svg {\n width: 18px;\n height: 18px;\n}\n\n/* \u2500\u2500 lists \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-list {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n list-style: none;\n margin: 0;\n padding: 8px 0 calc(env(safe-area-inset-bottom, 0px) + 16px);\n}\n\n.mobile-row {\n display: flex;\n align-items: center;\n gap: 10px;\n width: calc(100% - 24px);\n margin: 0 12px 8px;\n padding: 13px 14px;\n border: 1px solid var(--m-border);\n border-radius: 14px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.mobile-row:active {\n background: color-mix(in srgb, var(--m-text) 5%, var(--m-bg-raised));\n}\n\n.mobile-rowMain {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: baseline;\n gap: 8px;\n}\n\n.mobile-rowTitle {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n font-size: 15px;\n}\n\n.mobile-rowMeta {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.mobile-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 18px;\n}\n\n.mobile-live {\n margin-left: 6px;\n color: var(--m-success);\n font-size: 10px;\n vertical-align: middle;\n}\n\n/* \u2500\u2500 empty / error states \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 12px;\n padding: 32px;\n}\n\n.mobile-muted {\n margin: 0;\n color: var(--m-text-tertiary);\n}\n\n.mobile-error {\n margin: 0;\n color: var(--m-danger);\n}\n\n.mobile-pad {\n padding: 8px 16px;\n}\n\n.mobile-create {\n display: grid;\n gap: 8px;\n}\n\n.mobile-preset {\n display: grid;\n gap: 4px;\n}\n\n.mobile-presetLabel {\n color: var(--m-text-secondary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.mobile-presetSelect {\n width: 100%;\n height: 40px;\n padding: 0 36px 0 12px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n}\n\n.mobile-presetSelect:disabled {\n opacity: 0.6;\n}\n\n.mobile-presetDescription {\n margin: 0;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n/* \u2500\u2500 buttons \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-button {\n height: 38px;\n padding: 0 18px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-raised);\n color: var(--m-text);\n font-size: 14px;\n cursor: pointer;\n}\n\n.mobile-button:disabled {\n opacity: 0.5;\n}\n\n.mobile-block {\n display: block;\n width: calc(100% - 32px);\n margin: 4px 16px;\n}\n\n/* Primary action (create session): accent fill, readable on both themes. */\n.mobile-new {\n display: block;\n width: 100%;\n height: 44px;\n border: none;\n border-radius: 12px;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n font-size: 15px;\n font-weight: 600;\n cursor: pointer;\n box-shadow: 0 2px 8px rgba(47, 111, 237, 0.3);\n}\n\n.mobile-new:active {\n opacity: 0.88;\n}\n\n.mobile-new:disabled {\n opacity: 0.6;\n}\n\n.mobile-hint {\n display: block;\n margin-top: 2px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n/* \u2500\u2500 chat \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat {\n flex: 1;\n display: flex;\n flex-direction: column;\n min-height: 0;\n}\n\n.chat-scroll {\n flex: 1;\n overflow-y: auto;\n padding: 12px 14px calc(env(safe-area-inset-bottom, 0px) + 12px);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.chat-msg {\n max-width: 88%;\n padding: 10px 12px;\n border-radius: var(--m-radius);\n font-size: 14.5px;\n line-height: 1.55;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n.chat-msg-user {\n align-self: flex-end;\n background: linear-gradient(135deg, var(--m-accent) 0%, #4c8dff 100%);\n color: #fff;\n border-bottom-right-radius: 4px;\n box-shadow: 0 1px 3px rgba(47, 111, 237, 0.28);\n}\n\n.chat-msg-assistant {\n align-self: flex-start;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom-left-radius: 4px;\n box-shadow: var(--m-shadow);\n}\n\n.chat-msg-pending .chat-msg-text::after {\n content: '\u258D';\n animation: chat-blink 1s steps(2) infinite;\n}\n\n@keyframes chat-blink {\n 50% {\n opacity: 0;\n }\n}\n\n.chat-msg-failed {\n border-color: var(--m-danger);\n}\n\n.chat-msg-failtag {\n display: inline-block;\n margin-top: 6px;\n padding: 1px 8px;\n border: 1px solid var(--m-danger);\n border-radius: 999px;\n color: var(--m-danger);\n font-size: 11px;\n}\n\n.chat-msg-time {\n display: block;\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 10.5px;\n line-height: 1.2;\n}\n\n.chat-msg-toggle {\n display: block;\n margin-top: 8px;\n padding: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-accent);\n font-size: 13px;\n cursor: pointer;\n}\n\n/* Assistant markdown content (GFM subset; renderer in markdown.ts). */\n.chat-md {\n white-space: normal;\n}\n.chat-md-collapsed {\n max-height: 45vh;\n overflow: hidden;\n position: relative;\n}\n.chat-md-collapsed::after {\n content: '';\n position: absolute;\n left: 0;\n right: 0;\n bottom: 0;\n height: 28px;\n background: linear-gradient(transparent, var(--m-bg-raised));\n pointer-events: none;\n}\n.chat-md-body p { margin: 0 0 8px; }\n.chat-md-body p:last-child { margin-bottom: 0; }\n.chat-md-body h1, .chat-md-body h2, .chat-md-body h3,\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 {\n margin: 12px 0 6px;\n font-weight: 650;\n line-height: 1.3;\n}\n.chat-md-body h1 { font-size: 1.35em; }\n.chat-md-body h2 { font-size: 1.25em; }\n.chat-md-body h3 { font-size: 1.15em; }\n.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 { font-size: 1.05em; }\n.chat-md-body code {\n font-family: var(--m-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);\n font-size: 0.9em;\n background: var(--m-accent-soft);\n padding: 1px 5px;\n border-radius: 4px;\n}\n.chat-md-body pre {\n margin: 8px 0;\n padding: 10px;\n background: #101726;\n color: #dbe4f5;\n border-radius: 8px;\n overflow-x: auto;\n font-size: 12.5px;\n line-height: 1.5;\n}\n.chat-md-body pre code {\n background: transparent;\n padding: 0;\n color: inherit;\n}\n.chat-md-body ul, .chat-md-body ol { margin: 4px 0 8px; padding-left: 22px; }\n.chat-md-body li { margin: 2px 0; }\n.chat-md-body blockquote {\n margin: 8px 0;\n padding: 4px 10px;\n border-left: 3px solid var(--m-accent);\n background: var(--m-accent-soft);\n border-radius: 0 6px 6px 0;\n color: var(--m-text-secondary);\n}\n.chat-md-body table {\n margin: 8px 0;\n border-collapse: collapse;\n display: block;\n overflow-x: auto;\n font-size: 13px;\n}\n.chat-md-body th, .chat-md-body td {\n border: 1px solid var(--m-border);\n padding: 4px 8px;\n}\n.chat-md-body th { background: var(--m-accent-soft); }\n.chat-md-body a { color: var(--m-accent); }\n.chat-md-body hr { border: none; border-top: 1px solid var(--m-border); margin: 10px 0; }\n.chat-md-body img { max-width: 100%; border-radius: 8px; }\n\n/* \u2500\u2500 message disclosures (reasoning / tools) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-disclosure {\n margin-bottom: 8px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: color-mix(in srgb, var(--m-bg) 55%, transparent);\n overflow: hidden;\n}\n\n.chat-disclosure-head {\n display: flex;\n align-items: center;\n gap: 6px;\n width: 100%;\n padding: 7px 10px;\n border: none;\n background: transparent;\n color: var(--m-text);\n font-size: 12.5px;\n text-align: left;\n cursor: pointer;\n white-space: nowrap;\n}\n\n.chat-disclosure-caret {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n line-height: 1;\n transition: transform 0.15s ease;\n}\n\n.chat-disclosure-open .chat-disclosure-caret {\n transform: rotate(90deg);\n}\n\n.chat-disclosure-label {\n flex: none;\n color: var(--m-text-secondary);\n}\n\n.chat-reasoning[data-pending] .chat-disclosure-label {\n color: var(--m-accent);\n}\n\n.chat-disclosure-summary {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.chat-disclosure-count {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-disclosure-body {\n padding: 8px 10px 10px;\n border-top: 1px dashed var(--m-border);\n color: var(--m-text-secondary);\n font-size: 12.5px;\n line-height: 1.6;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n max-height: 40vh;\n overflow-y: auto;\n}\n\n.chat-tools-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.chat-tool-item {\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.chat-tool-name {\n color: var(--m-text);\n font-size: 12.5px;\n font-weight: 600;\n}\n\n.chat-tool-args {\n margin: 0;\n padding: 6px 8px;\n border-radius: 6px;\n background: var(--m-bg-input);\n color: var(--m-text-secondary);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n line-height: 1.5;\n overflow-wrap: break-word;\n white-space: pre-wrap;\n}\n\n/* \u2500\u2500 composer toolbar (model / permission chips) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-tools {\n display: flex;\n gap: 8px;\n padding: 6px 12px 0;\n}\n\n.chat-chip {\n flex: 1;\n min-width: 0;\n display: flex;\n align-items: center;\n gap: 6px;\n height: 32px;\n padding: 0 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n color: var(--m-text);\n font-size: 12px;\n cursor: pointer;\n}\n\n.chat-chip:active {\n border-color: var(--m-accent);\n background: var(--m-accent-soft);\n}\n\n.chat-chip-label {\n flex: none;\n color: var(--m-text-tertiary);\n}\n\n.chat-chip-value {\n flex: 1;\n min-width: 0;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.chat-chip-chevron {\n flex: none;\n color: var(--m-text-tertiary);\n font-size: 14px;\n}\n\n/* \u2500\u2500 bottom sheets \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-backdrop {\n position: fixed;\n inset: 0;\n z-index: 50;\n display: flex;\n flex-direction: column;\n justify-content: flex-end;\n background: var(--m-backdrop);\n animation: sheet-fade 0.18s ease;\n}\n\n@keyframes sheet-fade {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n.sheet {\n max-height: 74dvh;\n display: flex;\n flex-direction: column;\n background: var(--m-bg-raised);\n border: 1px solid var(--m-border);\n border-bottom: none;\n border-radius: 16px 16px 0 0;\n box-shadow: var(--m-shadow-raise);\n padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 10px);\n animation: sheet-up 0.22s ease;\n}\n\n@keyframes sheet-up {\n from {\n transform: translateY(40px);\n opacity: 0.6;\n }\n to {\n transform: translateY(0);\n opacity: 1;\n }\n}\n\n.sheet-handle {\n flex: none;\n align-self: center;\n width: 36px;\n height: 4px;\n margin: 8px 0 4px;\n border-radius: 999px;\n background: var(--m-border);\n}\n\n.sheet-title {\n flex: none;\n padding: 6px 16px 10px;\n font-size: 15px;\n font-weight: 600;\n}\n\n.sheet-body {\n flex: 1;\n min-height: 0;\n overflow-y: auto;\n padding: 0 12px 8px;\n}\n\n.sheet-status {\n padding: 18px 8px;\n color: var(--m-text-tertiary);\n font-size: 13px;\n text-align: center;\n display: flex;\n flex-direction: column;\n align-items: center;\n gap: 8px;\n}\n\n.sheet-status-error {\n color: var(--m-danger);\n}\n\n.sheet-hint {\n margin: 0;\n color: var(--m-text-secondary);\n font-size: 12px;\n line-height: 1.5;\n max-width: 32em;\n}\n\n.sheet-error {\n margin: 0 0 8px;\n color: var(--m-danger);\n font-size: 12.5px;\n}\n\n.sheet-section {\n margin-bottom: 14px;\n}\n\n.sheet-section-title {\n padding: 4px 8px 6px;\n color: var(--m-text-tertiary);\n font-size: 12px;\n font-weight: 600;\n}\n\n.sheet-option {\n display: flex;\n align-items: center;\n gap: 10px;\n width: 100%;\n padding: 10px 12px;\n border: 1px solid transparent;\n border-radius: 10px;\n background: transparent;\n color: var(--m-text);\n text-align: left;\n cursor: pointer;\n}\n\n.sheet-option:active {\n background: var(--m-bg-input);\n}\n\n.sheet-option-selected {\n border-color: var(--m-accent);\n background: color-mix(in srgb, var(--m-accent) 12%, transparent);\n}\n\n.sheet-option:disabled {\n opacity: 0.5;\n}\n\n.sheet-option-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-option-title {\n font-size: 14px;\n}\n\n.sheet-option-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-option-check {\n flex: none;\n color: var(--m-accent);\n font-size: 15px;\n font-weight: 700;\n}\n\n.sheet-confirm-desc {\n margin: 0 4px 14px;\n color: var(--m-text-secondary);\n font-size: 13px;\n line-height: 1.6;\n}\n\n.sheet-confirm-actions {\n display: flex;\n gap: 10px;\n padding: 4px;\n}\n\n.sheet-confirm-actions .mobile-button {\n flex: 1;\n}\n\n.sheet-confirm-danger {\n flex: 1;\n height: 38px;\n border: none;\n border-radius: 10px;\n background: var(--m-danger);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.sheet-confirm-danger:disabled {\n opacity: 0.5;\n}\n\n.chat-meta {\n margin-top: 4px;\n color: var(--m-text-tertiary);\n font-size: 11px;\n}\n\n.chat-typing {\n color: var(--m-text-tertiary);\n font-size: 13px;\n padding: 4px 2px;\n}\n\n.chat-inputbar {\n display: flex;\n gap: 8px;\n padding: 10px 12px calc(env(safe-area-inset-bottom, 0px) + 10px);\n border-top: 1px solid var(--m-border);\n background: var(--m-bg);\n}\n\n.chat-input {\n flex: 1;\n min-width: 0;\n min-height: 40px;\n max-height: 120px;\n /* Grow with the content (still bounded by min/max-height); browsers\n without field-sizing support ignore this line and keep the old size. */\n field-sizing: content;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 10px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n font-size: 14.5px;\n resize: none;\n outline: none;\n}\n\n.chat-input:focus {\n border-color: var(--m-accent);\n}\n\n.chat-send {\n flex: none;\n align-self: flex-end;\n height: 40px;\n padding: 0 16px;\n border: none;\n border-radius: 10px;\n background: var(--m-accent);\n color: #fff;\n font-size: 14px;\n cursor: pointer;\n}\n\n.chat-send:disabled {\n opacity: 0.5;\n}\n\n.chat-load-older {\n align-self: center;\n margin: 4px 0;\n border: none;\n background: transparent;\n color: var(--m-text-secondary);\n font-size: 13px;\n cursor: pointer;\n padding: 6px 10px;\n}\n\n/* Touch surfaces have no hover, so keyboard focus must be fully visible.\n One shared ring keeps every interactive control consistent and readable\n on both palettes. */\n.mobile-row:focus-visible,\n.mobile-button:focus-visible,\n.mobile-new:focus-visible,\n.mobile-presetSelect:focus-visible,\n.chat-send:focus-visible,\n.chat-chip:focus-visible,\n.chat-msg-toggle:focus-visible,\n.chat-load-older:focus-visible,\n.chat-disclosure-head:focus-visible,\n.sheet-option:focus-visible,\n.sheet-confirm-danger:focus-visible,\n.sheet-toggle-switch:focus-visible,\n.chat-input:focus-visible {\n outline: none;\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n/* \u2500\u2500 chat context usage chip (chat-tools row) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.chat-context {\n flex: none;\n align-self: center;\n padding: 2px 10px;\n border: 1px solid var(--m-border);\n border-radius: 999px;\n background: color-mix(in srgb, var(--m-text-tertiary) 10%, transparent);\n color: var(--m-text-tertiary);\n font-size: 11px;\n line-height: 1.6;\n white-space: nowrap;\n}\n\n.chat-context-warn {\n border-color: var(--m-danger);\n background: color-mix(in srgb, var(--m-danger) 14%, transparent);\n color: var(--m-danger);\n}\n\n/* \u2500\u2500 display sheet toggle rows \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.sheet-toggle-row {\n display: flex;\n align-items: center;\n gap: 12px;\n padding: 12px 8px;\n border-bottom: 1px solid var(--m-border);\n}\n\n.sheet-toggle-row:last-child {\n border-bottom: none;\n}\n\n.sheet-toggle-copy {\n flex: 1;\n min-width: 0;\n display: flex;\n flex-direction: column;\n gap: 2px;\n}\n\n.sheet-toggle-title {\n font-size: 14px;\n}\n\n.sheet-toggle-desc {\n color: var(--m-text-tertiary);\n font-size: 12px;\n line-height: 1.4;\n}\n\n.sheet-toggle-switch {\n position: relative;\n flex: none;\n width: 44px;\n height: 26px;\n border: none;\n border-radius: 999px;\n background: var(--m-border);\n cursor: pointer;\n transition: background-color 0.15s ease;\n}\n\n.sheet-toggle-switch-on {\n background: var(--m-accent);\n}\n\n.sheet-toggle-switch-knob {\n position: absolute;\n top: 3px;\n left: 3px;\n width: 20px;\n height: 20px;\n border-radius: 50%;\n background: #fff;\n box-shadow: 0 1px 2px rgba(16, 24, 40, 0.2);\n transition: transform 0.15s ease;\n}\n\n.sheet-toggle-switch-on .sheet-toggle-switch-knob {\n transform: translateX(18px);\n}\n\n/* Accessibility: collapse every interaction/enter animation for\n reduced-motion users; surface layout itself stays static. */\n@media (prefers-reduced-motion: reduce) {\n .mobile-theme-toggle,\n .mobile-back,\n .mobile-row,\n .mobile-button,\n .mobile-new,\n .chat-send,\n .chat-chip,\n .chat-msg-toggle,\n .chat-load-older,\n .chat-disclosure-head,\n .chat-disclosure-caret,\n .chat-msg-pending .chat-msg-text::after,\n .sheet-backdrop,\n .sheet,\n .sheet-option,\n .sheet-confirm-danger,\n .sheet-toggle-switch,\n .sheet-toggle-switch-knob,\n .chat-input {\n animation: none;\n transition: none;\n }\n}\n\n/* \u2500\u2500 installed-app pairing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n\n.mobile-pair {\n min-height: 100dvh;\n display: grid;\n place-items: center;\n padding: 24px;\n}\n\n.mobile-pairCard {\n display: grid;\n width: min(100%, 420px);\n gap: 12px;\n padding: 24px;\n border: 1px solid var(--m-border);\n border-radius: 12px;\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow-raise);\n}\n\n.mobile-pairLabel {\n color: var(--m-text-secondary);\n font-size: 13px;\n font-weight: 600;\n}\n\n.mobile-pairInput {\n width: 100%;\n min-height: 42px;\n box-sizing: border-box;\n padding: 9px 12px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-input);\n color: var(--m-text);\n font: inherit;\n outline: none;\n}\n\n.mobile-pairInput:focus-visible {\n border-color: var(--m-accent);\n box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);\n}\n\n.mobile-pairSubmit {\n width: 100%;\n}\n";
|
|
6
6
|
//# sourceMappingURL=mobile-styles.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-styles.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,SAAS,
|
|
1
|
+
{"version":3,"file":"mobile-styles.d.ts","sourceRoot":"","sources":["../../../src/mobile/mobile-styles.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,eAAO,MAAM,SAAS,mtxBAglCrB,CAAA"}
|
|
@@ -252,6 +252,43 @@ body {
|
|
|
252
252
|
padding: 8px 16px;
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
.mobile-create {
|
|
256
|
+
display: grid;
|
|
257
|
+
gap: 8px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.mobile-preset {
|
|
261
|
+
display: grid;
|
|
262
|
+
gap: 4px;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
.mobile-presetLabel {
|
|
266
|
+
color: var(--m-text-secondary);
|
|
267
|
+
font-size: 13px;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.mobile-presetSelect {
|
|
272
|
+
width: 100%;
|
|
273
|
+
height: 40px;
|
|
274
|
+
padding: 0 36px 0 12px;
|
|
275
|
+
border: 1px solid var(--m-border);
|
|
276
|
+
border-radius: 10px;
|
|
277
|
+
background: var(--m-bg-input);
|
|
278
|
+
color: var(--m-text);
|
|
279
|
+
font: inherit;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
.mobile-presetSelect:disabled {
|
|
283
|
+
opacity: 0.6;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.mobile-presetDescription {
|
|
287
|
+
margin: 0;
|
|
288
|
+
color: var(--m-text-tertiary);
|
|
289
|
+
font-size: 12px;
|
|
290
|
+
}
|
|
291
|
+
|
|
255
292
|
/* ── buttons ─────────────────────────────────────────────────────────── */
|
|
256
293
|
|
|
257
294
|
.mobile-button {
|
|
@@ -896,6 +933,7 @@ body {
|
|
|
896
933
|
.mobile-row:focus-visible,
|
|
897
934
|
.mobile-button:focus-visible,
|
|
898
935
|
.mobile-new:focus-visible,
|
|
936
|
+
.mobile-presetSelect:focus-visible,
|
|
899
937
|
.chat-send:focus-visible,
|
|
900
938
|
.chat-chip:focus-visible,
|
|
901
939
|
.chat-msg-toggle:focus-visible,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAKjE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGrC,+BAA+B;AAC/B,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAA;IACpB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC3B,MAAM,IAAI,IAAI,CAAA;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAA;AAqCzC,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;CACrB;AAqCD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,aAAa,+
|
|
1
|
+
{"version":3,"file":"ChatView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/ChatView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAKH,OAAO,EAAuB,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAKjE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAA;AAGrC,+BAA+B;AAC/B,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,WAAW,CAAA;IACpB,6EAA6E;IAC7E,GAAG,CAAC,EAAE,SAAS,GAAG,SAAS,CAAA;IAC3B,MAAM,IAAI,IAAI,CAAA;CACf;AAED;;;;GAIG;AACH,eAAO,MAAM,sBAAsB,MAAM,CAAA;AAqCzC,6EAA6E;AAC7E,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,iFAAiF;AACjF,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,gBAAgB,EAAE,CAAA;IAC3B,YAAY,EAAE,MAAM,CAAA;CACrB;AAqCD;;;;GAIG;AACH,wBAAgB,QAAQ,CAAC,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,aAAa,+BAmX/D"}
|
|
@@ -11,11 +11,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
11
11
|
* - a toolbar above the composer carries the model (+ thinking effort) and
|
|
12
12
|
* permission pickers, both as bottom sheets.
|
|
13
13
|
*/
|
|
14
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
14
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
15
15
|
import { loadHistory, prompt } from './App.js';
|
|
16
16
|
import { errorText, formatTime, staleHostHint } from './App.js';
|
|
17
17
|
import { fetchMobilePreferences, models, selectModel, sendCommand } from '../api.js';
|
|
18
|
-
import { foldEvents } from '../messages.js';
|
|
18
|
+
import { EventFolder, foldEvents } from '../messages.js';
|
|
19
19
|
import { renderMarkdown } from '../markdown.js';
|
|
20
20
|
import { ThemeToggle } from '../theme-toggle.js';
|
|
21
21
|
/**
|
|
@@ -119,6 +119,8 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
119
119
|
const tailLoadingRef = useRef(true);
|
|
120
120
|
/** Live session events buffered while the initial tail page loads. */
|
|
121
121
|
const liveBufferRef = useRef([]);
|
|
122
|
+
/** Incremental folder for this session's stream (indexes stay hot across events). */
|
|
123
|
+
const folderRef = useRef(undefined);
|
|
122
124
|
/** True once the live buffer hit its cap (oldest events were dropped). */
|
|
123
125
|
const liveBufferOverflowRef = useRef(false);
|
|
124
126
|
/** The session's permission select (absent = capability not composed). */
|
|
@@ -158,6 +160,7 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
158
160
|
tailLoadingRef.current = true;
|
|
159
161
|
liveBufferRef.current = [];
|
|
160
162
|
liveBufferOverflowRef.current = false;
|
|
163
|
+
folderRef.current = undefined;
|
|
161
164
|
setLoading(true);
|
|
162
165
|
setError(undefined);
|
|
163
166
|
setMessages([]);
|
|
@@ -169,7 +172,9 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
169
172
|
const buffered = liveBufferRef.current;
|
|
170
173
|
liveBufferRef.current = [];
|
|
171
174
|
tailLoadingRef.current = false;
|
|
172
|
-
|
|
175
|
+
const folder = new EventFolder(foldEvents(page.events.map(eventOf)));
|
|
176
|
+
folderRef.current = folder;
|
|
177
|
+
setMessages(folder.fold(buffered));
|
|
173
178
|
setHasOlder(page.hasMore);
|
|
174
179
|
setLoading(false);
|
|
175
180
|
// The history-tail projection baseline seeds the permission picker.
|
|
@@ -185,7 +190,9 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
185
190
|
void loadHistory(session.sessionId, undefined, controller.signal).then((fresh) => {
|
|
186
191
|
if (cancelled)
|
|
187
192
|
return;
|
|
188
|
-
|
|
193
|
+
const folder = folderRef.current;
|
|
194
|
+
const freshEvents = fresh.events.map(eventOf);
|
|
195
|
+
setMessages(previous => folder === undefined ? foldEvents(freshEvents, previous) : folder.fold(freshEvents));
|
|
189
196
|
liveBufferOverflowRef.current = false;
|
|
190
197
|
}, (reason) => {
|
|
191
198
|
if (cancelled)
|
|
@@ -200,8 +207,10 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
200
207
|
const buffered = liveBufferRef.current;
|
|
201
208
|
liveBufferRef.current = [];
|
|
202
209
|
tailLoadingRef.current = false;
|
|
203
|
-
if (buffered.length > 0)
|
|
204
|
-
|
|
210
|
+
if (buffered.length > 0) {
|
|
211
|
+
const folder = folderRef.current;
|
|
212
|
+
setMessages(folder === undefined ? foldEvents(buffered) : folder.fold(buffered));
|
|
213
|
+
}
|
|
205
214
|
setError(errorText(reason));
|
|
206
215
|
setLoading(false);
|
|
207
216
|
});
|
|
@@ -240,7 +249,10 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
240
249
|
liveBufferRef.current.push(event);
|
|
241
250
|
return;
|
|
242
251
|
}
|
|
243
|
-
setMessages(previous =>
|
|
252
|
+
setMessages(previous => {
|
|
253
|
+
const folder = folderRef.current;
|
|
254
|
+
return folder === undefined ? foldEvents([event], previous) : folder.fold([event]);
|
|
255
|
+
});
|
|
244
256
|
return;
|
|
245
257
|
}
|
|
246
258
|
// Live projection pushes keep the permission picker current.
|
|
@@ -295,7 +307,14 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
295
307
|
pendingRef.current = false;
|
|
296
308
|
setLoading(false);
|
|
297
309
|
const older = foldEvents(page.events.map(eventOf));
|
|
298
|
-
|
|
310
|
+
const folder = folderRef.current;
|
|
311
|
+
if (folder === undefined) {
|
|
312
|
+
setMessages(previous => [...older, ...previous]);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
folder.prepend(older);
|
|
316
|
+
setMessages(folder.snapshot());
|
|
317
|
+
}
|
|
299
318
|
setHasOlder(page.hasMore);
|
|
300
319
|
}, (reason) => {
|
|
301
320
|
pendingRef.current = false;
|
|
@@ -350,8 +369,12 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
350
369
|
}, onClose: () => { setSheet(null); } })), sheet === 'display' && (_jsx(DisplaySheet, { showToolCalls: showToolCalls, showSystemMessages: showSystemMessages, onToolCalls: (value) => { setShowToolCalls(value); writeStoredBoolean(SHOW_TOOL_CALLS_KEY, value); }, onSystemMessages: (value) => { setShowSystemMessages(value); writeStoredBoolean(SHOW_SYSTEM_MESSAGES_KEY, value); }, onClose: () => { setSheet(null); } }))] }));
|
|
351
370
|
}
|
|
352
371
|
/* ── message rows ─────────────────────────────────────────────────────── */
|
|
353
|
-
/**
|
|
354
|
-
|
|
372
|
+
/**
|
|
373
|
+
* One rendered message row (user bubble or assistant bubble with folds).
|
|
374
|
+
* Memoized: live streaming updates exactly one message object per frame, so
|
|
375
|
+
* unchanged rows skip re-rendering their markdown/sub-components.
|
|
376
|
+
*/
|
|
377
|
+
const MessageRow = memo(function MessageRow({ message, showToolCalls, showSystemMessages }) {
|
|
355
378
|
// Injected user messages (sourceKind defined and not 'user') hide behind
|
|
356
379
|
// the system-message toggle. Assistant messages are never hidden.
|
|
357
380
|
if (message.kind === 'user'
|
|
@@ -363,7 +386,7 @@ function MessageRow({ message, showToolCalls, showSystemMessages }) {
|
|
|
363
386
|
return (_jsxs("div", { className: `chat-msg chat-msg-${message.kind}${message.pending === true ? ' chat-msg-pending' : ''}${message.failed === true ? ' chat-msg-failed' : ''}`, children: [message.kind === 'assistant' && message.reasoning !== undefined && message.reasoning !== '' && (_jsx(ReasoningDisclosure, { text: message.reasoning, pending: message.pending === true })), showToolCalls && message.kind === 'assistant' && message.tools !== undefined && message.tools.length > 0 && (_jsx(ToolDisclosure, { tools: message.tools })), message.kind === 'assistant'
|
|
364
387
|
? _jsx(MarkdownText, { text: message.text })
|
|
365
388
|
: _jsx(CollapsibleText, { text: message.text }), message.failed === true && _jsx("span", { className: "chat-msg-failtag", children: "\u672C\u6B21\u56DE\u590D\u5931\u8D25" }), _jsx("span", { className: "chat-msg-time", children: formatTime(message.time) })] }));
|
|
366
|
-
}
|
|
389
|
+
});
|
|
367
390
|
/** Collapsed-by-default reasoning disclosure (web-UI Think-row parity). */
|
|
368
391
|
function ReasoningDisclosure({ text, pending }) {
|
|
369
392
|
const [open, setOpen] = useState(false);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"SessionListView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/SessionListView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;
|
|
1
|
+
{"version":3,"file":"SessionListView.d.ts","sourceRoot":"","sources":["../../../../src/mobile/views/SessionListView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,aAAa,IAAI,YAAY,EAAE,MAAM,8CAA8C,CAAA;AAIjG,OAAO,EAAuD,KAAK,WAAW,EAAE,MAAM,WAAW,CAAA;AAGjG,kCAAkC;AAClC,MAAM,WAAW,oBAAoB;IACnC,SAAS,EAAE,YAAY,CAAA;IACvB,MAAM,IAAI,IAAI,CAAA;IACd,MAAM,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CAAA;CACnC;AAUD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE,oBAAoB,+BAqMlF"}
|
|
@@ -12,7 +12,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
12
12
|
* the new chat — the same "new session opens" flow as the desktop UI.
|
|
13
13
|
*/
|
|
14
14
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
15
|
-
import { createSession, listSessions, listWorkspaces } from '../api.js';
|
|
15
|
+
import { createSession, listAgentPresets, listSessions, listWorkspaces } from '../api.js';
|
|
16
16
|
import { errorText, formatTime, staleHostHint, toSessionView } from './App.js';
|
|
17
17
|
import { ThemeToggle } from '../theme-toggle.js';
|
|
18
18
|
/** Rows that belong to the opened workspace (its owned session id set). */
|
|
@@ -34,6 +34,9 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
34
34
|
const [error, setError] = useState(undefined);
|
|
35
35
|
const [creating, setCreating] = useState(false);
|
|
36
36
|
const [createError, setCreateError] = useState(undefined);
|
|
37
|
+
const [presets, setPresets] = useState([]);
|
|
38
|
+
const [selectedPreset, setSelectedPreset] = useState(undefined);
|
|
39
|
+
const [presetsLoading, setPresetsLoading] = useState(true);
|
|
37
40
|
const cursorRef = useRef(undefined);
|
|
38
41
|
const busyRef = useRef(false);
|
|
39
42
|
// First page on mount. The workspace roster is re-read alongside so the
|
|
@@ -60,6 +63,25 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
60
63
|
});
|
|
61
64
|
return () => { cancelled = true; };
|
|
62
65
|
}, [workspace]);
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
let cancelled = false;
|
|
68
|
+
setPresets([]);
|
|
69
|
+
setSelectedPreset(undefined);
|
|
70
|
+
setPresetsLoading(true);
|
|
71
|
+
void listAgentPresets().then((roster) => {
|
|
72
|
+
if (cancelled)
|
|
73
|
+
return;
|
|
74
|
+
const usable = roster.presets.filter(preset => preset.broken === undefined);
|
|
75
|
+
setPresets(roster.presets);
|
|
76
|
+
setSelectedPreset((usable.find(preset => preset.isDefault) ?? usable[0])?.id);
|
|
77
|
+
setPresetsLoading(false);
|
|
78
|
+
}, () => {
|
|
79
|
+
if (cancelled)
|
|
80
|
+
return;
|
|
81
|
+
setPresetsLoading(false);
|
|
82
|
+
});
|
|
83
|
+
return () => { cancelled = true; };
|
|
84
|
+
}, [workspace]);
|
|
63
85
|
/** Pull the next page and append the rows that belong to this workspace. */
|
|
64
86
|
const loadMore = useCallback(() => {
|
|
65
87
|
if (busyRef.current)
|
|
@@ -87,7 +109,10 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
87
109
|
return;
|
|
88
110
|
setCreating(true);
|
|
89
111
|
setCreateError(undefined);
|
|
90
|
-
void createSession({
|
|
112
|
+
void createSession({
|
|
113
|
+
workspaceId: workspace.workspaceId,
|
|
114
|
+
...(selectedPreset !== undefined ? { agentPreset: selectedPreset } : {}),
|
|
115
|
+
}).then((created) => {
|
|
91
116
|
setCreating(false);
|
|
92
117
|
const view = {
|
|
93
118
|
sessionId: created.sessionId,
|
|
@@ -102,7 +127,8 @@ export function SessionListView({ workspace, onBack, onPick }) {
|
|
|
102
127
|
setCreating(false);
|
|
103
128
|
setCreateError(errorText(reason));
|
|
104
129
|
});
|
|
105
|
-
}, [creating, workspace, onPick]);
|
|
130
|
+
}, [creating, workspace, onPick, selectedPreset]);
|
|
106
131
|
const createHint = createError !== undefined ? staleHostHint(createError) : undefined;
|
|
107
|
-
|
|
132
|
+
const selectedPresetEntry = presets.find(preset => preset.id === selectedPreset);
|
|
133
|
+
return (_jsxs("div", { className: "mobile", children: [_jsxs("header", { className: "mobile-header", children: [_jsx("button", { type: "button", className: "mobile-back", "aria-label": "\u8FD4\u56DE", onClick: onBack, children: "\u2039" }), _jsx("h1", { className: "mobile-title mobile-titleInline", children: workspace.title }), _jsx(ThemeToggle, {})] }), error !== undefined && _jsx("p", { className: "mobile-error mobile-pad", children: error }), _jsxs("div", { className: "mobile-create mobile-pad", children: [presets.length > 0 && (_jsxs("label", { className: "mobile-preset", children: [_jsx("span", { className: "mobile-presetLabel", children: "Agent \u6A21\u5F0F" }), _jsxs("select", { className: "mobile-presetSelect", value: selectedPreset ?? '', disabled: selectedPreset === undefined, onChange: (event) => { setSelectedPreset(event.target.value); }, children: [selectedPreset === undefined && _jsx("option", { value: "", children: "\u65E0\u53EF\u7528 Agent \u6A21\u5F0F" }), presets.map(preset => (_jsxs("option", { value: preset.id, disabled: preset.broken !== undefined, children: [preset.name ?? preset.id, preset.isDefault ? '(默认)' : '', preset.trust === 'user' ? '(本地)' : '', preset.broken !== undefined ? '(不可用)' : ''] }, preset.id)))] })] })), selectedPresetEntry?.description !== undefined && (_jsx("p", { className: "mobile-presetDescription", children: selectedPresetEntry.description })), _jsx("button", { type: "button", className: "mobile-new", disabled: creating || presetsLoading, onClick: () => { void handleCreate(); }, children: creating ? '创建中…' : '+ 新建会话' })] }), createError !== undefined && (_jsxs("p", { className: "mobile-error mobile-pad", children: [createError, createHint !== undefined && _jsx("span", { className: "mobile-hint", children: createHint })] })), _jsx("ul", { className: "mobile-list", children: rows.map(row => (_jsx("li", { children: _jsxs("button", { type: "button", className: "mobile-row", onClick: () => { onPick(row); }, children: [_jsxs("span", { className: "mobile-rowMain", children: [_jsxs("span", { className: "mobile-rowTitle", children: [row.blank ? '新会话' : row.title, row.running ? _jsx("span", { className: "mobile-live", children: "\u25CF" }) : null] }), _jsx("span", { className: "mobile-rowMeta", children: formatTime(row.updatedAt) })] }), _jsx("span", { className: "mobile-chevron", children: "\u203A" })] }) }, row.sessionId))) }), hasMore && (_jsx("div", { className: "mobile-pad", children: _jsx("button", { type: "button", className: "mobile-button mobile-block", disabled: loading, onClick: () => { void loadMore(); }, children: loading ? '加载中…' : '加载更多会话' }) })), !hasMore && rows.length === 0 && !loading && (_jsx("div", { className: "mobile-empty", children: _jsx("p", { className: "mobile-muted", children: "\u8BE5\u5DE5\u4F5C\u533A\u8FD8\u6CA1\u6709\u4F1A\u8BDD\uFF0C\u70B9\u4E0A\u65B9\u6309\u94AE\u65B0\u5EFA\u4E00\u4E2A" }) }))] }));
|
|
108
134
|
}
|