@linxin666/dsh-remote-web-ui 0.3.2 → 0.3.4
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 +22 -8
- package/README.zh.md +12 -7
- package/assets/mobile-service-worker.js +1 -2
- package/lib/client.js +177 -19
- package/lib/client.js.map +1 -1
- package/lib/index.js +222 -15
- package/lib/mobile.js +64 -13
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +2 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +2 -2
- package/lib/types/client/RemoteEntry.js +1 -1
- package/lib/types/client/UpdatePanel.d.ts.map +1 -1
- package/lib/types/client/UpdatePanel.js +20 -3
- package/lib/types/client/index.d.ts +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +4 -0
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +22 -10
- package/lib/types/client/settings-form.d.ts +1 -1
- package/lib/types/client/settings-form.js +1 -1
- package/lib/types/client/telemetry.d.ts +27 -0
- package/lib/types/client/telemetry.d.ts.map +1 -0
- package/lib/types/client/telemetry.js +97 -0
- package/lib/types/dsh-home.d.ts.map +1 -1
- package/lib/types/dsh-home.js +9 -3
- package/lib/types/http.d.ts +1 -1
- package/lib/types/http.js +1 -1
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +34 -5
- package/lib/types/loopback.d.ts +1 -1
- package/lib/types/loopback.js +1 -1
- package/lib/types/mobile/api.d.ts +7 -0
- package/lib/types/mobile/api.d.ts.map +1 -1
- package/lib/types/mobile/api.js +9 -3
- 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 +10 -0
- package/lib/types/mobile/views/ChatView.d.ts +1 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +32 -5
- package/lib/types/mobile/views/ChatView.stream.test.js +28 -2
- package/lib/types/mobile/views/ChatView.test.js +152 -2
- package/lib/types/mobile-api.d.ts +14 -0
- package/lib/types/mobile-api.d.ts.map +1 -1
- package/lib/types/mobile-api.js +56 -1
- package/lib/types/mobile-routes.d.ts.map +1 -1
- package/lib/types/mobile-routes.js +25 -2
- package/lib/types/mount-once.d.ts +1 -1
- package/lib/types/mount-once.js +2 -2
- package/lib/types/update.d.ts +35 -4
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +98 -4
- package/package.json +4 -2
- package/src/client/PluginSettingsCard.tsx +4 -2
- package/src/client/RemoteEntry.tsx +1 -1
- package/src/client/UpdatePanel.tsx +43 -3
- package/src/client/index.ts +6 -1
- package/src/client/locales.ts +22 -10
- package/src/client/remote.module.css +56 -3
- package/src/client/settings-card.module.css +5 -0
- package/src/client/settings-form.ts +1 -1
- package/src/client/telemetry.ts +105 -0
- package/src/dsh-home.ts +9 -3
- package/src/http.ts +1 -1
- package/src/index.ts +36 -4
- package/src/loopback.ts +1 -1
- package/src/mobile/api.ts +10 -3
- package/src/mobile/mobile-styles.ts +10 -0
- package/src/mobile/views/ChatView.stream.test.tsx +34 -2
- package/src/mobile/views/ChatView.test.tsx +177 -2
- package/src/mobile/views/ChatView.tsx +44 -6
- package/src/mobile-api.ts +61 -0
- package/src/mobile-routes.ts +24 -2
- package/src/mount-once.ts +2 -2
- package/src/update.ts +117 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/mobile/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAA;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kDAAkD,CAAA;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAA;AAGzH,6BAA6B;AAC7B,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACpC,UAAU,EAAE,OAAO,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,6CAA6C,EAAE,YAAY,EAAE,CAAA;IAC5E,OAAO,EAAE,OAAO,CAAA;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAA;CACtC;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AAED,kEAAkE;AAClE,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAG/D;AAED,yFAAyF;AACzF,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAEzE;AAED,iEAAiE;AACjE,wBAAsB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAExE;AAED,+DAA+D;AAC/D,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAEnE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GACzE,OAAO,CAAC,cAAc,CAAC,CAEzB;AAED,oFAAoF;AACpF,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,SAAK,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED,qEAAqE;AACrE,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3E;AAED,wEAAwE;AACxE,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../../src/mobile/api.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,8CAA8C,CAAA;AACjF,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kDAAkD,CAAA;AACxF,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,uBAAuB,EAAE,MAAM,6CAA6C,CAAA;AAGzH,6BAA6B;AAC7B,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,cAAc,EAAE,CAAA;IACvB,+DAA+D;IAC/D,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,gFAAgF;AAChF,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAA;IACjB,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAA;CACrB;AAED,4DAA4D;AAC5D,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,SAAS,gBAAgB,EAAE,CAAA;IACpC,UAAU,EAAE,OAAO,CAAA;IACnB,WAAW,EAAE,OAAO,CAAA;CACrB;AAED,wEAAwE;AACxE,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,OAAO,6CAA6C,EAAE,YAAY,EAAE,CAAA;IAC5E,OAAO,EAAE,OAAO,CAAA;IAChB;;;OAGG;IACH,WAAW,CAAC,EAAE,uBAAuB,CAAA;CACtC;AAED,4EAA4E;AAC5E,MAAM,WAAW,iBAAiB;IAChC,6EAA6E;IAC7E,iBAAiB,EAAE,OAAO,CAAA;CAC3B;AAED,kEAAkE;AAClE,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CAG/D;AAED,yFAAyF;AACzF,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAEzE;AAED,iEAAiE;AACjE,wBAAsB,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAExE;AAED,+DAA+D;AAC/D,wBAAsB,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC,CAEnE;AAED;;;GAGG;AACH,wBAAsB,aAAa,CACjC,OAAO,GAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAO,GACzE,OAAO,CAAC,cAAc,CAAC,CAEzB;AAED,oFAAoF;AACpF,wBAAsB,OAAO,CAC3B,SAAS,EAAE,MAAM,EACjB,SAAS,CAAC,EAAE,MAAM,EAClB,WAAW,SAAK,EAChB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,WAAW,CAAC,CAMtB;AAED,qEAAqE;AACrE,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM3E;AAED,wEAAwE;AACxE,wBAAsB,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAKnF;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,IAAI,CAAA;CAAE,CAAC,CAElF;AAED,oFAAoF;AACpF,wBAAsB,MAAM,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,CAAC,CAEtE;AAED,2FAA2F;AAC3F,wBAAsB,WAAW,CAC/B,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,GACvE,OAAO,CAAC;IAAE,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,eAAe,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC,CAOtF;AAID,8DAA8D;AAC9D,MAAM,WAAW,eAAe;IAC9B,UAAU,EAAE,MAAM,CAAA;IAClB,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,6DAA6D;AAC7D,MAAM,WAAW,mBAAmB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,QAAQ,EAAE,MAAM,CAAA;IAChB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,OAAO,CAAC,EAAE,KAAK,CAAC;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IACxD,WAAW,CAAC,EAAE,OAAO,CAAA;CACtB;AAED,yCAAyC;AACzC,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,eAAe,EAAE,CAAA;IAC5B,SAAS,EAAE,mBAAmB,EAAE,CAAA;CACjC;AAED,4FAA4F;AAC5F,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAE3E;AAED,8DAA8D;AAC9D,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,cAAc,GAAG,UAAU,GACnC,OAAO,CAAC,IAAI,CAAC,CAOf;AAED,0CAA0C;AAC1C,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,KAAK,CAAC;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,GAClE,OAAO,CAAC,IAAI,CAAC,CAMf;AAID,wCAAwC;AACxC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,OAAO,CAAA;CAChB;AAED,gFAAgF;AAChF,MAAM,WAAW,gBAAgB;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;IACZ,MAAM,EAAE,cAAc,EAAE,CAAA;IACxB,OAAO,EAAE,cAAc,EAAE,CAAA;IACzB,SAAS,EAAE,OAAO,CAAA;CACnB;AAED,qEAAqE;AACrE,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,aAAa,CAAA;IACxB,OAAO,EAAE,OAAO,CAAA;CACjB;AAED,2EAA2E;AAC3E,wBAAsB,aAAa,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAE5E;AAED,2EAA2E;AAC3E,wBAAsB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAElF"}
|
package/lib/types/mobile/api.js
CHANGED
|
@@ -47,12 +47,18 @@ export async function prompt(sessionId, text) {
|
|
|
47
47
|
}
|
|
48
48
|
/** Send one slash command line (e.g. `/permission workspace-write`). */
|
|
49
49
|
export async function sendCommand(sessionId, line) {
|
|
50
|
-
return await callUnary('
|
|
50
|
+
return await callUnary('mobile.command', {
|
|
51
51
|
sessionId,
|
|
52
|
-
|
|
53
|
-
content: [{ type: 'text', text: line }],
|
|
52
|
+
line,
|
|
54
53
|
});
|
|
55
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Stop the session's active turn (the mobile stop button). Pending queued
|
|
57
|
+
* work is preserved and resumes in FIFO order once cancellation settles.
|
|
58
|
+
*/
|
|
59
|
+
export async function cancelSession(sessionId) {
|
|
60
|
+
return await callUnary('session.cancel', { sessionId });
|
|
61
|
+
}
|
|
56
62
|
/** Fresh advisory model directory for one session (current + groups + failures). */
|
|
57
63
|
export async function models(sessionId) {
|
|
58
64
|
return await callUnary('session.models', { sessionId });
|
|
@@ -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.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-card {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 6px 8px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-raised);\n}\n\n.chat-tool-pills {\n display: flex;\n gap: 4px;\n flex-wrap: wrap;\n align-items: center;\n}\n\n.chat-tool-pill {\n display: inline-block;\n padding: 1px 7px;\n border-radius: 999px;\n background: var(--m-accent-soft);\n color: var(--m-accent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n font-weight: 600;\n white-space: nowrap;\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 max-height: 200px;\n overflow-y: auto;\n}\n\n/* \u2500\u2500 turn status indicator (#1017) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-turn-status {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 10px 16px;\n color: var(--m-accent);\n font-size: 13px;\n font-weight: 600;\n background: linear-gradient(\n 90deg,\n var(--m-accent) 0%,\n color-mix(in srgb, var(--m-accent) 60%, #ffffff) 50%,\n var(--m-accent) 100%\n );\n background-size: 200% 100%;\n background-clip: text;\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n animation: dsh-shimmer 1.8s linear infinite;\n}\n\n.chat-turn-dots {\n display: inline-flex;\n gap: 3px;\n}\n\n.chat-turn-dots span {\n display: inline-block;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: var(--m-accent);\n animation: dsh-dot-bounce 1.2s ease-in-out infinite;\n}\n\n.chat-turn-dots span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.chat-turn-dots span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dsh-shimmer {\n from { background-position: 200% 0; }\n to { background-position: -200% 0; }\n}\n\n@keyframes dsh-dot-bounce {\n 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }\n 40% { transform: translateY(-4px); opacity: 1; }\n}\n\n/* \u2500\u2500 approval panel (#1025) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-approval-panel {\n margin: 8px 12px;\n padding: 10px 12px;\n border: 1px solid var(--m-accent);\n border-radius: var(--m-radius);\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n}\n\n.chat-approval-header {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 8px;\n}\n\n.chat-approval-reason {\n color: var(--m-text-secondary);\n font-size: 12.5px;\n}\n\n.chat-approval-error {\n margin: 4px 0;\n color: var(--m-danger);\n font-size: 12px;\n}\n\n.chat-approval-actions {\n display: flex;\n gap: 8px;\n}\n\n.chat-approval-allow {\n flex: 1;\n height: 34px;\n border: none;\n border-radius: 8px;\n background: var(--m-accent);\n color: #fff;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.chat-approval-allow:disabled {\n opacity: 0.6;\n}\n\n.chat-approval-reject {\n flex: 1;\n height: 34px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-raised);\n color: var(--m-text);\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n\n.chat-approval-reject:disabled {\n opacity: 0.6;\n}\n\n/* \u2500\u2500 question panel (#1025) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-question-panel {\n margin: 8px 12px;\n padding: 10px 12px;\n border: 1px solid var(--m-accent);\n border-radius: var(--m-radius);\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.chat-question-group {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.chat-question-header {\n font-size: 14px;\n font-weight: 600;\n color: var(--m-text);\n}\n\n.chat-question-text {\n font-size: 13px;\n color: var(--m-text);\n line-height: 1.5;\n}\n\n.chat-question-detail {\n font-size: 12px;\n color: var(--m-text-secondary);\n line-height: 1.5;\n}\n\n.chat-question-options {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.chat-question-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n cursor: pointer;\n font-size: 13px;\n transition: border-color 0.12s, background 0.12s;\n}\n\n.chat-question-option input {\n margin: 2px 0 0;\n flex: none;\n}\n\n.chat-question-option-selected {\n border-color: var(--m-accent);\n background: var(--m-accent-soft);\n}\n\n.chat-question-option-label {\n color: var(--m-text);\n font-weight: 500;\n}\n\n.chat-question-option-desc {\n display: block;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.chat-question-custom {\n width: 100%;\n min-height: 40px;\n box-sizing: border-box;\n padding: 8px 10px;\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 font-size: 13px;\n resize: vertical;\n outline: none;\n}\n\n.chat-question-custom:focus-visible {\n border-color: var(--m-accent);\n}\n\n.chat-question-submit {\n height: 36px;\n border: none;\n border-radius: 8px;\n background: var(--m-accent);\n color: #fff;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.chat-question-submit:disabled {\n opacity: 0.6;\n}\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 .chat-turn-status,\n .chat-turn-dots span {\n animation: none;\n transition: none;\n }\n .chat-turn-status {\n background: none;\n -webkit-text-fill-color: var(--m-accent);\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\n/* \u2500\u2500 directory browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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.dir-browser {\n display: flex;\n flex-direction: column;\n}\n\n.dir-crumbs {\n display: flex;\n align-items: center;\n overflow-x: auto;\n padding: 12px 16px;\n background: var(--m-bg-raised);\n border-bottom: 1px solid var(--m-border);\n white-space: nowrap;\n}\n\n.dir-crumbs::-webkit-scrollbar {\n display: none;\n}\n\n.dir-crumb {\n border: none;\n background: transparent;\n padding: 4px 6px;\n color: var(--m-text);\n font-size: 14px;\n cursor: pointer;\n}\n\n.dir-crumb:active {\n background: var(--m-bg-input);\n border-radius: 4px;\n}\n\n.dir-crumb-separator {\n color: var(--m-text-tertiary);\n margin: 0 4px;\n font-size: 14px;\n}\n\n.dir-entry {\n padding-left: 16px;\n}\n\n.dir-entry-hidden {\n opacity: 0.5;\n}\n\n.dir-select {\n padding: 16px;\n background: var(--m-bg);\n border-top: 1px solid var(--m-border);\n}\n\n.dir-empty {\n padding: 40px 0;\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-card {\n display: flex;\n flex-direction: column;\n gap: 4px;\n padding: 6px 8px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-raised);\n}\n\n.chat-tool-pills {\n display: flex;\n gap: 4px;\n flex-wrap: wrap;\n align-items: center;\n}\n\n.chat-tool-pill {\n display: inline-block;\n padding: 1px 7px;\n border-radius: 999px;\n background: var(--m-accent-soft);\n color: var(--m-accent);\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n font-size: 11px;\n font-weight: 600;\n white-space: nowrap;\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 max-height: 200px;\n overflow-y: auto;\n}\n\n/* \u2500\u2500 turn status indicator (#1017) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-turn-status {\n display: flex;\n align-items: center;\n gap: 6px;\n padding: 10px 16px;\n color: var(--m-accent);\n font-size: 13px;\n font-weight: 600;\n background: linear-gradient(\n 90deg,\n var(--m-accent) 0%,\n color-mix(in srgb, var(--m-accent) 60%, #ffffff) 50%,\n var(--m-accent) 100%\n );\n background-size: 200% 100%;\n background-clip: text;\n -webkit-background-clip: text;\n -webkit-text-fill-color: transparent;\n animation: dsh-shimmer 1.8s linear infinite;\n}\n\n.chat-turn-dots {\n display: inline-flex;\n gap: 3px;\n}\n\n.chat-turn-dots span {\n display: inline-block;\n width: 5px;\n height: 5px;\n border-radius: 50%;\n background: var(--m-accent);\n animation: dsh-dot-bounce 1.2s ease-in-out infinite;\n}\n\n.chat-turn-dots span:nth-child(2) {\n animation-delay: 0.15s;\n}\n\n.chat-turn-dots span:nth-child(3) {\n animation-delay: 0.3s;\n}\n\n@keyframes dsh-shimmer {\n from { background-position: 200% 0; }\n to { background-position: -200% 0; }\n}\n\n@keyframes dsh-dot-bounce {\n 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }\n 40% { transform: translateY(-4px); opacity: 1; }\n}\n\n/* \u2500\u2500 approval panel (#1025) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-approval-panel {\n margin: 8px 12px;\n padding: 10px 12px;\n border: 1px solid var(--m-accent);\n border-radius: var(--m-radius);\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n}\n\n.chat-approval-header {\n display: flex;\n align-items: center;\n gap: 8px;\n margin-bottom: 8px;\n}\n\n.chat-approval-reason {\n color: var(--m-text-secondary);\n font-size: 12.5px;\n}\n\n.chat-approval-error {\n margin: 4px 0;\n color: var(--m-danger);\n font-size: 12px;\n}\n\n.chat-approval-actions {\n display: flex;\n gap: 8px;\n}\n\n.chat-approval-allow {\n flex: 1;\n height: 34px;\n border: none;\n border-radius: 8px;\n background: var(--m-accent);\n color: #fff;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.chat-approval-allow:disabled {\n opacity: 0.6;\n}\n\n.chat-approval-reject {\n flex: 1;\n height: 34px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n background: var(--m-bg-raised);\n color: var(--m-text);\n font: inherit;\n font-size: 13px;\n cursor: pointer;\n}\n\n.chat-approval-reject:disabled {\n opacity: 0.6;\n}\n\n/* \u2500\u2500 question panel (#1025) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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-question-panel {\n margin: 8px 12px;\n padding: 10px 12px;\n border: 1px solid var(--m-accent);\n border-radius: var(--m-radius);\n background: var(--m-bg-raised);\n box-shadow: var(--m-shadow);\n display: flex;\n flex-direction: column;\n gap: 10px;\n}\n\n.chat-question-group {\n display: flex;\n flex-direction: column;\n gap: 6px;\n}\n\n.chat-question-header {\n font-size: 14px;\n font-weight: 600;\n color: var(--m-text);\n}\n\n.chat-question-text {\n font-size: 13px;\n color: var(--m-text);\n line-height: 1.5;\n}\n\n.chat-question-detail {\n font-size: 12px;\n color: var(--m-text-secondary);\n line-height: 1.5;\n}\n\n.chat-question-options {\n display: flex;\n flex-direction: column;\n gap: 4px;\n}\n\n.chat-question-option {\n display: flex;\n align-items: flex-start;\n gap: 8px;\n padding: 8px 10px;\n border: 1px solid var(--m-border);\n border-radius: 8px;\n cursor: pointer;\n font-size: 13px;\n transition: border-color 0.12s, background 0.12s;\n}\n\n.chat-question-option input {\n margin: 2px 0 0;\n flex: none;\n}\n\n.chat-question-option-selected {\n border-color: var(--m-accent);\n background: var(--m-accent-soft);\n}\n\n.chat-question-option-label {\n color: var(--m-text);\n font-weight: 500;\n}\n\n.chat-question-option-desc {\n display: block;\n color: var(--m-text-tertiary);\n font-size: 12px;\n}\n\n.chat-question-custom {\n width: 100%;\n min-height: 40px;\n box-sizing: border-box;\n padding: 8px 10px;\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 font-size: 13px;\n resize: vertical;\n outline: none;\n}\n\n.chat-question-custom:focus-visible {\n border-color: var(--m-accent);\n}\n\n.chat-question-submit {\n height: 36px;\n border: none;\n border-radius: 8px;\n background: var(--m-accent);\n color: #fff;\n font: inherit;\n font-size: 13px;\n font-weight: 600;\n cursor: pointer;\n}\n\n.chat-question-submit:disabled {\n opacity: 0.6;\n}\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/* Running state: the primary button carries only the square stop icon, so\n it centers the glyph instead of sizing to text. */\n.chat-send-stop {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 44px;\n padding: 0 12px;\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 .chat-turn-status,\n .chat-turn-dots span {\n animation: none;\n transition: none;\n }\n .chat-turn-status {\n background: none;\n -webkit-text-fill-color: var(--m-accent);\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\n/* \u2500\u2500 directory browser \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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.dir-browser {\n display: flex;\n flex-direction: column;\n}\n\n.dir-crumbs {\n display: flex;\n align-items: center;\n overflow-x: auto;\n padding: 12px 16px;\n background: var(--m-bg-raised);\n border-bottom: 1px solid var(--m-border);\n white-space: nowrap;\n}\n\n.dir-crumbs::-webkit-scrollbar {\n display: none;\n}\n\n.dir-crumb {\n border: none;\n background: transparent;\n padding: 4px 6px;\n color: var(--m-text);\n font-size: 14px;\n cursor: pointer;\n}\n\n.dir-crumb:active {\n background: var(--m-bg-input);\n border-radius: 4px;\n}\n\n.dir-crumb-separator {\n color: var(--m-text-tertiary);\n margin: 0 4px;\n font-size: 14px;\n}\n\n.dir-entry {\n padding-left: 16px;\n}\n\n.dir-entry-hidden {\n opacity: 0.5;\n}\n\n.dir-select {\n padding: 16px;\n background: var(--m-bg);\n border-top: 1px solid var(--m-border);\n}\n\n.dir-empty {\n padding: 40px 0;\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,w4/BAy5CrB,CAAA"}
|
|
@@ -1170,6 +1170,16 @@ body {
|
|
|
1170
1170
|
opacity: 0.5;
|
|
1171
1171
|
}
|
|
1172
1172
|
|
|
1173
|
+
/* Running state: the primary button carries only the square stop icon, so
|
|
1174
|
+
it centers the glyph instead of sizing to text. */
|
|
1175
|
+
.chat-send-stop {
|
|
1176
|
+
display: inline-flex;
|
|
1177
|
+
align-items: center;
|
|
1178
|
+
justify-content: center;
|
|
1179
|
+
min-width: 44px;
|
|
1180
|
+
padding: 0 12px;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1173
1183
|
.chat-load-older {
|
|
1174
1184
|
align-self: center;
|
|
1175
1185
|
margin: 4px 0;
|
|
@@ -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;AAMjE,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;AAMjE,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,+BAoe/D;AA2GD;;;;;;;;GAQG;AACH,eAAO,MAAM,yBAAyB,MAAM,CAAA;AA6F5C,eAAO,MAAM,eAAe,OAAO,CAAA"}
|
|
@@ -14,7 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
14
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
|
-
import { fetchMobilePreferences, models, selectModel, sendCommand, fetchPending, respondApproval, respondQuestion } from "../api.js";
|
|
17
|
+
import { fetchMobilePreferences, models, selectModel, sendCommand, cancelSession, fetchPending, respondApproval, respondQuestion } from "../api.js";
|
|
18
18
|
import { EventFolder, foldEvents } from "../messages.js";
|
|
19
19
|
import { renderMarkdown } from "../markdown.js";
|
|
20
20
|
import { ThemeToggle } from "../theme-toggle.js";
|
|
@@ -140,6 +140,8 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
140
140
|
const [mobileEnterToSend, setMobileEnterToSend] = useState(true);
|
|
141
141
|
/** Whether the assistant is currently generating (turn/start..turn/end). */
|
|
142
142
|
const [running, setRunning] = useState(false);
|
|
143
|
+
/** Whether a stop request is in flight (guards the composer's stop button). */
|
|
144
|
+
const [stopping, setStopping] = useState(false);
|
|
143
145
|
/** Pending tool approvals awaiting user decision (#1025). */
|
|
144
146
|
const [pendingApprovals, setPendingApprovals] = useState([]);
|
|
145
147
|
/** Pending questions awaiting user answer (#1025). */
|
|
@@ -398,6 +400,21 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
398
400
|
setError(errorText(reason));
|
|
399
401
|
});
|
|
400
402
|
}, [input, sending, session.sessionId]);
|
|
403
|
+
/**
|
|
404
|
+
* Stop the active turn (desktop parity: the composer's primary button
|
|
405
|
+
* becomes a stop button while running). The turn/end frame arriving over
|
|
406
|
+
* mux flips the button back; a failed request surfaces through the chat
|
|
407
|
+
* error line.
|
|
408
|
+
*/
|
|
409
|
+
const stopTurn = useCallback(() => {
|
|
410
|
+
if (stopping)
|
|
411
|
+
return;
|
|
412
|
+
setStopping(true);
|
|
413
|
+
void cancelSession(session.sessionId).then(() => { setStopping(false); }, (reason) => {
|
|
414
|
+
setStopping(false);
|
|
415
|
+
setError(errorText(reason));
|
|
416
|
+
});
|
|
417
|
+
}, [stopping, session.sessionId]);
|
|
401
418
|
const modelLabel = currentModel?.model ?? '模型';
|
|
402
419
|
const permissionLabel = permissions === undefined
|
|
403
420
|
? undefined
|
|
@@ -426,7 +443,10 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
426
443
|
event.preventDefault();
|
|
427
444
|
void send();
|
|
428
445
|
}
|
|
429
|
-
} }), _jsx("button", { type: "button", className:
|
|
446
|
+
} }), _jsx("button", { type: "button", className: running ? 'chat-send chat-send-stop' : 'chat-send', ...(running ? { 'aria-label': stopping ? '停止中' : '停止' } : {}), disabled: running ? stopping : sending || input.trim() === '', onClick: () => { if (running)
|
|
447
|
+
void stopTurn();
|
|
448
|
+
else
|
|
449
|
+
void send(); }, children: running ? (_jsx("svg", { viewBox: "0 0 16 16", width: "14", height: "14", "aria-hidden": "true", focusable: "false", children: _jsx("rect", { x: "3", y: "3", width: "10", height: "10", rx: "3", fill: "currentColor" }) })) : sending ? '发送中…' : '发送' })] }), sheet === 'model' && (_jsx(ModelSheet, { sessionId: session.sessionId, current: currentModel, onCurrent: (selection) => { setCurrentModel(selection); }, onClose: () => { setSheet(null); } })), sheet === 'permission' && permissions !== undefined && (_jsx(PermissionSheet, { sessionId: session.sessionId, value: permissions, onChanged: (value) => {
|
|
430
450
|
setPermissions(previous => previous === undefined ? previous : { ...previous, currentValue: value });
|
|
431
451
|
}, 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); } }))] }));
|
|
432
452
|
}
|
|
@@ -438,13 +458,20 @@ export function ChatView({ session, mux, onBack }) {
|
|
|
438
458
|
*/
|
|
439
459
|
const MessageRow = memo(function MessageRow({ message, showToolCalls, showSystemMessages }) {
|
|
440
460
|
// Injected user messages (sourceKind defined and not 'user') hide behind
|
|
441
|
-
// the system-message toggle.
|
|
461
|
+
// the system-message toggle.
|
|
442
462
|
if (message.kind === 'user'
|
|
443
463
|
&& message.sourceKind !== undefined
|
|
444
464
|
&& message.sourceKind !== 'user'
|
|
445
465
|
&& !showSystemMessages) {
|
|
446
466
|
return null;
|
|
447
467
|
}
|
|
468
|
+
const hasReasoning = message.kind === 'assistant' && message.reasoning !== undefined && message.reasoning !== '';
|
|
469
|
+
const hasTools = showToolCalls && message.kind === 'assistant' && message.tools !== undefined && message.tools.length > 0;
|
|
470
|
+
const hasText = message.text !== '';
|
|
471
|
+
const hasFailTag = message.failed === true;
|
|
472
|
+
if (!hasReasoning && !hasTools && !hasText && !hasFailTag) {
|
|
473
|
+
return null;
|
|
474
|
+
}
|
|
448
475
|
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'
|
|
449
476
|
? _jsx(MarkdownText, { text: message.text, pending: message.pending === true })
|
|
450
477
|
: _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) })] }));
|
|
@@ -532,7 +559,7 @@ function MarkdownText({ text, pending }) {
|
|
|
532
559
|
clearTimeout(timerRef.current);
|
|
533
560
|
};
|
|
534
561
|
}, []);
|
|
535
|
-
const long = text.length > LONG_TEXT_LIMIT;
|
|
562
|
+
const long = !pending && text.length > LONG_TEXT_LIMIT;
|
|
536
563
|
const collapsed = long && !open;
|
|
537
564
|
return (_jsxs("div", { className: 'chat-msg-text chat-md' + (collapsed ? ' chat-md-collapsed' : ''), children: [_jsx("div", { className: "chat-md-body", dangerouslySetInnerHTML: { __html: html } }), long && (_jsx("button", { type: "button", className: "chat-msg-toggle", onClick: () => { setOpen(value => !value); }, children: open ? '收起' : '展开全文(' + text.length + ' 字)' }))] }));
|
|
538
565
|
}
|
|
@@ -545,7 +572,7 @@ function CollapsibleText({ text }) {
|
|
|
545
572
|
const shown = open ? text : text.slice(0, LONG_TEXT_PREVIEW);
|
|
546
573
|
return (_jsxs("span", { className: "chat-msg-text", children: [shown, !open ? '…' : '', _jsx("button", { type: "button", className: "chat-msg-toggle", onClick: () => { setOpen(value => !value); }, children: open ? '收起' : `展开全文(${text.length} 字)` })] }));
|
|
547
574
|
}
|
|
548
|
-
const LONG_TEXT_LIMIT =
|
|
575
|
+
export const LONG_TEXT_LIMIT = 6000;
|
|
549
576
|
const LONG_TEXT_PREVIEW = 800;
|
|
550
577
|
/** Latest non-empty line of a streaming reasoning buffer. */
|
|
551
578
|
function lastLine(text) {
|
|
@@ -8,8 +8,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
8
8
|
* to the plain renderMarkdown output) the moment the turn closes.
|
|
9
9
|
*/
|
|
10
10
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
-
import { act, cleanup, render } from '@testing-library/react';
|
|
12
|
-
import { ChatView, STREAM_RENDER_INTERVAL_MS } from "./ChatView.js";
|
|
11
|
+
import { act, cleanup, render, screen } from '@testing-library/react';
|
|
12
|
+
import { ChatView, STREAM_RENDER_INTERVAL_MS, LONG_TEXT_LIMIT } from "./ChatView.js";
|
|
13
13
|
import { renderMarkdown } from "../markdown.js";
|
|
14
14
|
// The api module is fully mocked; App.tsx's history wrapper is overridden to
|
|
15
15
|
// feed a fixed empty history page, its pure helpers stay real.
|
|
@@ -18,6 +18,8 @@ vi.mock('../api.ts', () => ({
|
|
|
18
18
|
models: vi.fn(),
|
|
19
19
|
selectModel: vi.fn(),
|
|
20
20
|
sendCommand: vi.fn(),
|
|
21
|
+
cancelSession: vi.fn(),
|
|
22
|
+
fetchPending: vi.fn(),
|
|
21
23
|
}));
|
|
22
24
|
vi.mock('./App.tsx', async (importOriginal) => {
|
|
23
25
|
const actual = await importOriginal();
|
|
@@ -202,4 +204,28 @@ describe('ChatView streaming markdown throttle', () => {
|
|
|
202
204
|
expect(renderMarkdownMock.mock.calls.length - beforeFinal).toBe(0);
|
|
203
205
|
expect(normalize(body().innerHTML)).toBe(normalize(renderMarkdown(full)));
|
|
204
206
|
});
|
|
207
|
+
it('does not collapse long text while streaming (pending) and collapses once turn finishes', async () => {
|
|
208
|
+
const mux = new FakeMux();
|
|
209
|
+
let container;
|
|
210
|
+
await act(async () => {
|
|
211
|
+
const rendered = render(_jsx(ChatView, { session: session, mux: mux, onBack: () => { } }));
|
|
212
|
+
container = rendered.container;
|
|
213
|
+
});
|
|
214
|
+
const hugeChunk = 'C'.repeat(LONG_TEXT_LIMIT + 200);
|
|
215
|
+
// Stream a chunk exceeding LONG_TEXT_LIMIT
|
|
216
|
+
await act(async () => {
|
|
217
|
+
chunk(mux, hugeChunk, 6);
|
|
218
|
+
vi.advanceTimersByTime(STREAM_RENDER_INTERVAL_MS + 5);
|
|
219
|
+
});
|
|
220
|
+
// During streaming (pending), message is not collapsed
|
|
221
|
+
expect(container.querySelector('.chat-md-collapsed')).toBeNull();
|
|
222
|
+
expect(screen.queryByRole('button', { name: /展开全文/ })).toBeNull();
|
|
223
|
+
// Turn closes
|
|
224
|
+
await act(async () => {
|
|
225
|
+
finalMessage(mux, hugeChunk, 7);
|
|
226
|
+
});
|
|
227
|
+
// Once turn ends (not pending), message is collapsed and has expand button
|
|
228
|
+
expect(container.querySelector('.chat-md-collapsed')).not.toBeNull();
|
|
229
|
+
expect(screen.getByRole('button', { name: new RegExp(`展开全文(${LONG_TEXT_LIMIT + 200} 字)`) })).toBeTruthy();
|
|
230
|
+
});
|
|
205
231
|
});
|
|
@@ -3,7 +3,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
3
3
|
/** ChatView: collapsible message folds, toolbar chips, and the bottom sheets. */
|
|
4
4
|
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
5
|
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
6
|
-
import { ChatView, MAX_TAIL_BUFFER_EVENTS } from "./ChatView.js";
|
|
6
|
+
import { ChatView, MAX_TAIL_BUFFER_EVENTS, LONG_TEXT_LIMIT } from "./ChatView.js";
|
|
7
7
|
// The api module is fully mocked; App.tsx's history wrapper is overridden to
|
|
8
8
|
// feed fixed history pages, its pure helpers (errorText / formatTime) stay real.
|
|
9
9
|
vi.mock('../api.ts', () => ({
|
|
@@ -11,6 +11,8 @@ vi.mock('../api.ts', () => ({
|
|
|
11
11
|
models: vi.fn(),
|
|
12
12
|
selectModel: vi.fn(),
|
|
13
13
|
sendCommand: vi.fn(),
|
|
14
|
+
cancelSession: vi.fn(),
|
|
15
|
+
fetchPending: vi.fn(),
|
|
14
16
|
}));
|
|
15
17
|
vi.mock('./App.tsx', async (importOriginal) => {
|
|
16
18
|
const actual = await importOriginal();
|
|
@@ -20,7 +22,7 @@ vi.mock('./App.tsx', async (importOriginal) => {
|
|
|
20
22
|
prompt: vi.fn(async () => { }),
|
|
21
23
|
};
|
|
22
24
|
});
|
|
23
|
-
import { fetchMobilePreferences, models, selectModel, sendCommand } from "../api.js";
|
|
25
|
+
import { fetchMobilePreferences, models, selectModel, sendCommand, cancelSession, fetchPending } from "../api.js";
|
|
24
26
|
import { loadHistory, prompt } from "./App.js";
|
|
25
27
|
const session = {
|
|
26
28
|
sessionId: 's-1',
|
|
@@ -37,6 +39,18 @@ function makeEntry(type, data, seq) {
|
|
|
37
39
|
function historyPage(events, extra = {}) {
|
|
38
40
|
return { events: events, hasMore: false, ...extra };
|
|
39
41
|
}
|
|
42
|
+
/** Minimal mux stand-in: captures the ChatView's frame listener for hand-off. */
|
|
43
|
+
class FakeMux {
|
|
44
|
+
listeners = new Set();
|
|
45
|
+
onFrame(listener) {
|
|
46
|
+
this.listeners.add(listener);
|
|
47
|
+
return () => { this.listeners.delete(listener); };
|
|
48
|
+
}
|
|
49
|
+
emit(frame) {
|
|
50
|
+
for (const listener of this.listeners)
|
|
51
|
+
listener(frame);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
40
54
|
/** A full turn: user message, reasoning + text chunks, tool calls, final message. */
|
|
41
55
|
function turnEvents() {
|
|
42
56
|
return [
|
|
@@ -63,6 +77,8 @@ const fetchMobilePreferencesMock = vi.mocked(fetchMobilePreferences);
|
|
|
63
77
|
const modelsMock = vi.mocked(models);
|
|
64
78
|
const selectModelMock = vi.mocked(selectModel);
|
|
65
79
|
const sendCommandMock = vi.mocked(sendCommand);
|
|
80
|
+
const cancelSessionMock = vi.mocked(cancelSession);
|
|
81
|
+
const fetchPendingMock = vi.mocked(fetchPending);
|
|
66
82
|
const loadHistoryMock = vi.mocked(loadHistory);
|
|
67
83
|
const promptMock = vi.mocked(prompt);
|
|
68
84
|
beforeEach(() => {
|
|
@@ -85,6 +101,8 @@ beforeEach(() => {
|
|
|
85
101
|
});
|
|
86
102
|
selectModelMock.mockResolvedValue({ selected: { provider: 'fx', model: 'fx-2', reasoningEffort: 'high' } });
|
|
87
103
|
sendCommandMock.mockResolvedValue({});
|
|
104
|
+
cancelSessionMock.mockResolvedValue({ accepted: true });
|
|
105
|
+
fetchPendingMock.mockResolvedValue({ approvals: [], questions: [] });
|
|
88
106
|
});
|
|
89
107
|
afterEach(() => {
|
|
90
108
|
cleanup();
|
|
@@ -566,3 +584,135 @@ describe('ChatView display toggles and context usage', () => {
|
|
|
566
584
|
expect(screen.queryByText(/上下文/)).toBeNull();
|
|
567
585
|
});
|
|
568
586
|
});
|
|
587
|
+
describe('ChatView stop button (#1041)', () => {
|
|
588
|
+
/** Emit one session lifecycle frame for the chat's session. */
|
|
589
|
+
function emitSessionEvent(mux, type, seq) {
|
|
590
|
+
act(() => {
|
|
591
|
+
mux.emit({ type: 'session/event', sessionId: 's-1', event: makeEntry(type, {}, seq).event });
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
it('switches the composer primary to a stop button while running and cancels the turn', async () => {
|
|
595
|
+
loadHistoryMock.mockResolvedValue(historyPage([]));
|
|
596
|
+
const mux = new FakeMux();
|
|
597
|
+
render(_jsx(ChatView, { session: session, mux: mux, onBack: () => { } }));
|
|
598
|
+
// Idle: the primary button is the (empty-draft disabled) send button.
|
|
599
|
+
const sendButton = await screen.findByRole('button', { name: '发送' });
|
|
600
|
+
expect(sendButton.disabled).toBe(true);
|
|
601
|
+
// Turn starts: the primary becomes an enabled stop button (square icon).
|
|
602
|
+
emitSessionEvent(mux, 'turn/start', 1);
|
|
603
|
+
const stopButton = (await screen.findByRole('button', { name: '停止' }));
|
|
604
|
+
expect(stopButton.disabled).toBe(false);
|
|
605
|
+
fireEvent.click(stopButton);
|
|
606
|
+
await waitFor(() => expect(cancelSessionMock).toHaveBeenCalledWith('s-1'));
|
|
607
|
+
// Turn ends: the primary flips back to send.
|
|
608
|
+
emitSessionEvent(mux, 'turn/end', 2);
|
|
609
|
+
expect(await screen.findByRole('button', { name: '发送' })).toBeTruthy();
|
|
610
|
+
});
|
|
611
|
+
it('disables the stop button while the cancel request is in flight', async () => {
|
|
612
|
+
loadHistoryMock.mockResolvedValue(historyPage([]));
|
|
613
|
+
let resolveCancel;
|
|
614
|
+
cancelSessionMock.mockReturnValue(new Promise((resolve) => {
|
|
615
|
+
resolveCancel = () => { resolve({ accepted: true }); };
|
|
616
|
+
}));
|
|
617
|
+
const mux = new FakeMux();
|
|
618
|
+
render(_jsx(ChatView, { session: session, mux: mux, onBack: () => { } }));
|
|
619
|
+
await screen.findByRole('button', { name: '发送' });
|
|
620
|
+
emitSessionEvent(mux, 'turn/start', 1);
|
|
621
|
+
const stopButton = (await screen.findByRole('button', { name: '停止' }));
|
|
622
|
+
fireEvent.click(stopButton);
|
|
623
|
+
// In flight: disabled and re-labeled; a second tap cannot double-submit.
|
|
624
|
+
const inflight = (await screen.findByRole('button', { name: '停止中' }));
|
|
625
|
+
expect(inflight.disabled).toBe(true);
|
|
626
|
+
fireEvent.click(inflight);
|
|
627
|
+
expect(cancelSessionMock).toHaveBeenCalledTimes(1);
|
|
628
|
+
await act(async () => { resolveCancel?.(); });
|
|
629
|
+
emitSessionEvent(mux, 'turn/end', 2);
|
|
630
|
+
expect(await screen.findByRole('button', { name: '发送' })).toBeTruthy();
|
|
631
|
+
});
|
|
632
|
+
it('surfaces a cancel failure through the chat error line', async () => {
|
|
633
|
+
loadHistoryMock.mockResolvedValue(historyPage([]));
|
|
634
|
+
cancelSessionMock.mockRejectedValue(new Error('cancel exploded'));
|
|
635
|
+
const mux = new FakeMux();
|
|
636
|
+
render(_jsx(ChatView, { session: session, mux: mux, onBack: () => { } }));
|
|
637
|
+
await screen.findByRole('button', { name: '发送' });
|
|
638
|
+
emitSessionEvent(mux, 'turn/start', 1);
|
|
639
|
+
fireEvent.click(await screen.findByRole('button', { name: '停止' }));
|
|
640
|
+
expect(await screen.findByText(/cancel exploded/)).toBeTruthy();
|
|
641
|
+
});
|
|
642
|
+
});
|
|
643
|
+
describe('ChatView message visibility and long text folding (#1065)', () => {
|
|
644
|
+
const toolOnlyEvents = () => [
|
|
645
|
+
makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: '查询文件' }] }, 0),
|
|
646
|
+
makeEntry('assistant/message', {
|
|
647
|
+
turn: 0,
|
|
648
|
+
step: 0,
|
|
649
|
+
message: { id: 'a-1', role: 'assistant', content: [] },
|
|
650
|
+
}, 1),
|
|
651
|
+
makeEntry('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'read_file', arguments: '{"path":"/a"}' }, 2),
|
|
652
|
+
];
|
|
653
|
+
it('hides assistant message completely (no air bubble) when only tool calls exist and tool toggle is off', async () => {
|
|
654
|
+
loadHistoryMock.mockResolvedValue(historyPage(toolOnlyEvents()));
|
|
655
|
+
const { container } = render(_jsx(ChatView, { session: session, onBack: () => { } }));
|
|
656
|
+
expect(await screen.findByText('查询文件')).toBeTruthy();
|
|
657
|
+
// By default showToolCalls is true, tool disclosure is visible
|
|
658
|
+
expect(await screen.findByRole('button', { name: /工具/ })).toBeTruthy();
|
|
659
|
+
// Turn off tool-calls toggle
|
|
660
|
+
fireEvent.click(screen.getByRole('button', { name: /显示/ }));
|
|
661
|
+
const toolSwitch = await screen.findByRole('switch', { name: '工具调用' });
|
|
662
|
+
fireEvent.click(toolSwitch);
|
|
663
|
+
// The tool disclosure is gone, and the entire assistant message bubble is not rendered (no air bubble)
|
|
664
|
+
expect(screen.queryByRole('button', { name: /工具/ })).toBeNull();
|
|
665
|
+
const msgElements = container.querySelectorAll('.chat-msg');
|
|
666
|
+
expect(msgElements.length).toBe(1);
|
|
667
|
+
expect(msgElements[0]?.classList.contains('chat-msg-user')).toBe(true);
|
|
668
|
+
});
|
|
669
|
+
it('renders failed tag even if assistant message has no text or reasoning', async () => {
|
|
670
|
+
loadHistoryMock.mockResolvedValue(historyPage([
|
|
671
|
+
makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: '测试失败' }] }, 0),
|
|
672
|
+
makeEntry('assistant/message', {
|
|
673
|
+
turn: 0,
|
|
674
|
+
step: 0,
|
|
675
|
+
message: { id: 'a-1', role: 'assistant', content: [] },
|
|
676
|
+
}, 1),
|
|
677
|
+
makeEntry('turn/end', { turn: 0, reason: { kind: 'error', message: 'timeout' } }, 2),
|
|
678
|
+
]));
|
|
679
|
+
render(_jsx(ChatView, { session: session, onBack: () => { } }));
|
|
680
|
+
expect(await screen.findByText('测试失败')).toBeTruthy();
|
|
681
|
+
expect(await screen.findByText('本次回复失败')).toBeTruthy();
|
|
682
|
+
});
|
|
683
|
+
it('collapses terminal assistant text exceeding LONG_TEXT_LIMIT and toggles open/close', async () => {
|
|
684
|
+
const longText = 'A'.repeat(LONG_TEXT_LIMIT + 100);
|
|
685
|
+
loadHistoryMock.mockResolvedValue(historyPage([
|
|
686
|
+
makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: '生成长文本' }] }, 0),
|
|
687
|
+
makeEntry('assistant/message', {
|
|
688
|
+
turn: 0,
|
|
689
|
+
step: 0,
|
|
690
|
+
message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: longText }] },
|
|
691
|
+
}, 1),
|
|
692
|
+
]));
|
|
693
|
+
const { container } = render(_jsx(ChatView, { session: session, onBack: () => { } }));
|
|
694
|
+
expect(await screen.findByText('生成长文本')).toBeTruthy();
|
|
695
|
+
const toggleButton = await screen.findByRole('button', { name: new RegExp(`展开全文(${LONG_TEXT_LIMIT + 100} 字)`) });
|
|
696
|
+
expect(toggleButton).toBeTruthy();
|
|
697
|
+
expect(container.querySelector('.chat-md-collapsed')).not.toBeNull();
|
|
698
|
+
// Expand
|
|
699
|
+
fireEvent.click(toggleButton);
|
|
700
|
+
expect(await screen.findByRole('button', { name: '收起' })).toBeTruthy();
|
|
701
|
+
expect(container.querySelector('.chat-md-collapsed')).toBeNull();
|
|
702
|
+
});
|
|
703
|
+
it('does not collapse terminal assistant text within LONG_TEXT_LIMIT', async () => {
|
|
704
|
+
const shortText = 'B'.repeat(LONG_TEXT_LIMIT);
|
|
705
|
+
loadHistoryMock.mockResolvedValue(historyPage([
|
|
706
|
+
makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: '生成中等文本' }] }, 0),
|
|
707
|
+
makeEntry('assistant/message', {
|
|
708
|
+
turn: 0,
|
|
709
|
+
step: 0,
|
|
710
|
+
message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: shortText }] },
|
|
711
|
+
}, 1),
|
|
712
|
+
]));
|
|
713
|
+
const { container } = render(_jsx(ChatView, { session: session, onBack: () => { } }));
|
|
714
|
+
expect(await screen.findByText('生成中等文本')).toBeTruthy();
|
|
715
|
+
expect(screen.queryByRole('button', { name: /展开全文/ })).toBeNull();
|
|
716
|
+
expect(container.querySelector('.chat-md-collapsed')).toBeNull();
|
|
717
|
+
});
|
|
718
|
+
});
|
|
@@ -37,6 +37,20 @@ export interface MobileApiDeps {
|
|
|
37
37
|
mobileEnterToSend: () => boolean;
|
|
38
38
|
/** SSE keep-alive ping cadence for the mux stream (default 15000 ms; test seam). */
|
|
39
39
|
eventsHeartbeatMs?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Optional command dispatcher for executing slash commands on the mobile
|
|
42
|
+
* surface. When absent, `mobile.command` returns an error; the mobile UI
|
|
43
|
+
* can still fall back to `session.prompt` (which sends the line to the
|
|
44
|
+
* model, not the command registry).
|
|
45
|
+
*/
|
|
46
|
+
commandDispatcher?: {
|
|
47
|
+
execute(sessionId: string, line: string, signal: AbortSignal): Promise<{
|
|
48
|
+
ok: true;
|
|
49
|
+
result: unknown;
|
|
50
|
+
} | {
|
|
51
|
+
error: string;
|
|
52
|
+
}>;
|
|
53
|
+
};
|
|
40
54
|
}
|
|
41
55
|
/** Mobile API route paths. */
|
|
42
56
|
export declare const MOBILE_API_PATHS: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;
|
|
1
|
+
{"version":3,"file":"mobile-api.d.ts","sourceRoot":"","sources":["../../src/mobile-api.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,iCAAiC,CAAA;AAC/D,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,gCAAgC,CAAA;AAG9D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAA;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,cAAc,CAAA;AAgElD,iCAAiC;AACjC,MAAM,WAAW,aAAa;IAC5B,uDAAuD;IACvD,OAAO,EAAE,cAAc,CAAA;IACvB,0DAA0D;IAC1D,QAAQ,EAAE,QAAQ,CAAA;IAClB,2BAA2B;IAC3B,cAAc,EAAE,cAAc,CAAA;IAC9B,kEAAkE;IAClE,iBAAiB,EAAE,MAAM,OAAO,CAAA;IAChC,oFAAoF;IACpF,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE;QAClB,OAAO,CACL,SAAS,EAAE,MAAM,EACjB,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,WAAW,GAClB,OAAO,CAAC;YAAE,EAAE,EAAE,IAAI,CAAC;YAAC,MAAM,EAAE,OAAO,CAAA;SAAE,GAAG;YAAE,KAAK,EAAE,MAAM,CAAA;SAAE,CAAC,CAAA;KAC9D,CAAA;CACF;AAED,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB;;CAEnB,CAAA;AAOV;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,aAAa,GAAG,QAAQ,EAAE,CA2NnE"}
|