@linxin666/dsh-remote-web-ui 0.1.17 → 0.1.19
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 +5 -2
- package/README.zh.md +2 -2
- package/lib/client.js +82 -66
- package/lib/client.js.map +1 -1
- package/lib/index.js +18 -18
- package/lib/mobile.js +649 -45
- package/lib/mobile.js.map +1 -1
- package/lib/types/client/PluginSettingsCard.d.ts +11 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +11 -5
- package/lib/types/client/RemoteSettingsCard.d.ts +5 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +7 -0
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +13 -7
- package/lib/types/client/settings-form.d.ts +8 -0
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +15 -1
- package/lib/types/gate.d.ts +4 -9
- package/lib/types/gate.d.ts.map +1 -1
- package/lib/types/gate.js +5 -32
- package/lib/types/loopback.d.ts +25 -0
- package/lib/types/loopback.d.ts.map +1 -0
- package/lib/types/loopback.js +68 -0
- package/lib/types/mobile/markdown.d.ts +26 -0
- package/lib/types/mobile/markdown.d.ts.map +1 -0
- package/lib/types/mobile/markdown.js +272 -0
- package/lib/types/mobile/markdown.test.d.ts +2 -0
- package/lib/types/mobile/markdown.test.d.ts.map +1 -0
- package/lib/types/mobile/markdown.test.js +67 -0
- package/lib/types/mobile/messages.d.ts +14 -0
- package/lib/types/mobile/messages.d.ts.map +1 -1
- package/lib/types/mobile/messages.js +54 -2
- package/lib/types/mobile/messages.test.js +99 -0
- 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 +167 -0
- package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
- package/lib/types/mobile/views/ChatView.js +81 -5
- package/lib/types/mobile/views/ChatView.test.js +105 -0
- package/package.json +1 -1
- package/src/client/PluginSettingsCard.tsx +50 -47
- package/src/client/RemoteSettingsCard.tsx +8 -0
- package/src/client/index.ts +13 -7
- package/src/client/settings-card.module.css +9 -0
- package/src/client/settings-form.ts +15 -1
- package/src/gate.ts +5 -30
- package/src/loopback.ts +63 -0
- package/src/mobile/markdown.test.ts +77 -0
- package/src/mobile/markdown.ts +278 -0
- package/src/mobile/messages.test.ts +106 -0
- package/src/mobile/messages.ts +66 -2
- package/src/mobile/mobile-styles.ts +167 -0
- package/src/mobile/views/ChatView.test.tsx +121 -0
- package/src/mobile/views/ChatView.tsx +164 -6
|
@@ -393,6 +393,85 @@ body {
|
|
|
393
393
|
cursor: pointer;
|
|
394
394
|
}
|
|
395
395
|
|
|
396
|
+
/* Assistant markdown content (GFM subset; renderer in markdown.ts). */
|
|
397
|
+
.chat-md {
|
|
398
|
+
white-space: normal;
|
|
399
|
+
}
|
|
400
|
+
.chat-md-collapsed {
|
|
401
|
+
max-height: 45vh;
|
|
402
|
+
overflow: hidden;
|
|
403
|
+
position: relative;
|
|
404
|
+
}
|
|
405
|
+
.chat-md-collapsed::after {
|
|
406
|
+
content: '';
|
|
407
|
+
position: absolute;
|
|
408
|
+
left: 0;
|
|
409
|
+
right: 0;
|
|
410
|
+
bottom: 0;
|
|
411
|
+
height: 28px;
|
|
412
|
+
background: linear-gradient(transparent, var(--m-bg-raised));
|
|
413
|
+
pointer-events: none;
|
|
414
|
+
}
|
|
415
|
+
.chat-md-body p { margin: 0 0 8px; }
|
|
416
|
+
.chat-md-body p:last-child { margin-bottom: 0; }
|
|
417
|
+
.chat-md-body h1, .chat-md-body h2, .chat-md-body h3,
|
|
418
|
+
.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 {
|
|
419
|
+
margin: 12px 0 6px;
|
|
420
|
+
font-weight: 650;
|
|
421
|
+
line-height: 1.3;
|
|
422
|
+
}
|
|
423
|
+
.chat-md-body h1 { font-size: 1.35em; }
|
|
424
|
+
.chat-md-body h2 { font-size: 1.25em; }
|
|
425
|
+
.chat-md-body h3 { font-size: 1.15em; }
|
|
426
|
+
.chat-md-body h4, .chat-md-body h5, .chat-md-body h6 { font-size: 1.05em; }
|
|
427
|
+
.chat-md-body code {
|
|
428
|
+
font-family: var(--m-mono, ui-monospace, SFMono-Regular, Menlo, Consolas, monospace);
|
|
429
|
+
font-size: 0.9em;
|
|
430
|
+
background: var(--m-accent-soft);
|
|
431
|
+
padding: 1px 5px;
|
|
432
|
+
border-radius: 4px;
|
|
433
|
+
}
|
|
434
|
+
.chat-md-body pre {
|
|
435
|
+
margin: 8px 0;
|
|
436
|
+
padding: 10px;
|
|
437
|
+
background: #101726;
|
|
438
|
+
color: #dbe4f5;
|
|
439
|
+
border-radius: 8px;
|
|
440
|
+
overflow-x: auto;
|
|
441
|
+
font-size: 12.5px;
|
|
442
|
+
line-height: 1.5;
|
|
443
|
+
}
|
|
444
|
+
.chat-md-body pre code {
|
|
445
|
+
background: transparent;
|
|
446
|
+
padding: 0;
|
|
447
|
+
color: inherit;
|
|
448
|
+
}
|
|
449
|
+
.chat-md-body ul, .chat-md-body ol { margin: 4px 0 8px; padding-left: 22px; }
|
|
450
|
+
.chat-md-body li { margin: 2px 0; }
|
|
451
|
+
.chat-md-body blockquote {
|
|
452
|
+
margin: 8px 0;
|
|
453
|
+
padding: 4px 10px;
|
|
454
|
+
border-left: 3px solid var(--m-accent);
|
|
455
|
+
background: var(--m-accent-soft);
|
|
456
|
+
border-radius: 0 6px 6px 0;
|
|
457
|
+
color: var(--m-text-secondary);
|
|
458
|
+
}
|
|
459
|
+
.chat-md-body table {
|
|
460
|
+
margin: 8px 0;
|
|
461
|
+
border-collapse: collapse;
|
|
462
|
+
display: block;
|
|
463
|
+
overflow-x: auto;
|
|
464
|
+
font-size: 13px;
|
|
465
|
+
}
|
|
466
|
+
.chat-md-body th, .chat-md-body td {
|
|
467
|
+
border: 1px solid var(--m-border);
|
|
468
|
+
padding: 4px 8px;
|
|
469
|
+
}
|
|
470
|
+
.chat-md-body th { background: var(--m-accent-soft); }
|
|
471
|
+
.chat-md-body a { color: var(--m-accent); }
|
|
472
|
+
.chat-md-body hr { border: none; border-top: 1px solid var(--m-border); margin: 10px 0; }
|
|
473
|
+
.chat-md-body img { max-width: 100%; border-radius: 8px; }
|
|
474
|
+
|
|
396
475
|
/* ── message disclosures (reasoning / tools) ─────────────────────────── */
|
|
397
476
|
|
|
398
477
|
.chat-disclosure {
|
|
@@ -824,11 +903,97 @@ body {
|
|
|
824
903
|
.chat-disclosure-head:focus-visible,
|
|
825
904
|
.sheet-option:focus-visible,
|
|
826
905
|
.sheet-confirm-danger:focus-visible,
|
|
906
|
+
.sheet-toggle-switch:focus-visible,
|
|
827
907
|
.chat-input:focus-visible {
|
|
828
908
|
outline: none;
|
|
829
909
|
box-shadow: 0 0 0 2px var(--m-bg), 0 0 0 4px var(--m-accent);
|
|
830
910
|
}
|
|
831
911
|
|
|
912
|
+
/* ── chat context usage chip (chat-tools row) ───────────────────────── */
|
|
913
|
+
|
|
914
|
+
.chat-context {
|
|
915
|
+
flex: none;
|
|
916
|
+
align-self: center;
|
|
917
|
+
padding: 2px 10px;
|
|
918
|
+
border: 1px solid var(--m-border);
|
|
919
|
+
border-radius: 999px;
|
|
920
|
+
background: color-mix(in srgb, var(--m-text-tertiary) 10%, transparent);
|
|
921
|
+
color: var(--m-text-tertiary);
|
|
922
|
+
font-size: 11px;
|
|
923
|
+
line-height: 1.6;
|
|
924
|
+
white-space: nowrap;
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
.chat-context-warn {
|
|
928
|
+
border-color: var(--m-danger);
|
|
929
|
+
background: color-mix(in srgb, var(--m-danger) 14%, transparent);
|
|
930
|
+
color: var(--m-danger);
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
/* ── display sheet toggle rows ──────────────────────────────────────── */
|
|
934
|
+
|
|
935
|
+
.sheet-toggle-row {
|
|
936
|
+
display: flex;
|
|
937
|
+
align-items: center;
|
|
938
|
+
gap: 12px;
|
|
939
|
+
padding: 12px 8px;
|
|
940
|
+
border-bottom: 1px solid var(--m-border);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
.sheet-toggle-row:last-child {
|
|
944
|
+
border-bottom: none;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
.sheet-toggle-copy {
|
|
948
|
+
flex: 1;
|
|
949
|
+
min-width: 0;
|
|
950
|
+
display: flex;
|
|
951
|
+
flex-direction: column;
|
|
952
|
+
gap: 2px;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
.sheet-toggle-title {
|
|
956
|
+
font-size: 14px;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
.sheet-toggle-desc {
|
|
960
|
+
color: var(--m-text-tertiary);
|
|
961
|
+
font-size: 12px;
|
|
962
|
+
line-height: 1.4;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
.sheet-toggle-switch {
|
|
966
|
+
position: relative;
|
|
967
|
+
flex: none;
|
|
968
|
+
width: 44px;
|
|
969
|
+
height: 26px;
|
|
970
|
+
border: none;
|
|
971
|
+
border-radius: 999px;
|
|
972
|
+
background: var(--m-border);
|
|
973
|
+
cursor: pointer;
|
|
974
|
+
transition: background-color 0.15s ease;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
.sheet-toggle-switch-on {
|
|
978
|
+
background: var(--m-accent);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
.sheet-toggle-switch-knob {
|
|
982
|
+
position: absolute;
|
|
983
|
+
top: 3px;
|
|
984
|
+
left: 3px;
|
|
985
|
+
width: 20px;
|
|
986
|
+
height: 20px;
|
|
987
|
+
border-radius: 50%;
|
|
988
|
+
background: #fff;
|
|
989
|
+
box-shadow: 0 1px 2px rgba(16, 24, 40, 0.2);
|
|
990
|
+
transition: transform 0.15s ease;
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
.sheet-toggle-switch-on .sheet-toggle-switch-knob {
|
|
994
|
+
transform: translateX(18px);
|
|
995
|
+
}
|
|
996
|
+
|
|
832
997
|
/* Accessibility: collapse every interaction/enter animation for
|
|
833
998
|
reduced-motion users; surface layout itself stays static. */
|
|
834
999
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -848,6 +1013,8 @@ body {
|
|
|
848
1013
|
.sheet,
|
|
849
1014
|
.sheet-option,
|
|
850
1015
|
.sheet-confirm-danger,
|
|
1016
|
+
.sheet-toggle-switch,
|
|
1017
|
+
.sheet-toggle-switch-knob,
|
|
851
1018
|
.chat-input {
|
|
852
1019
|
animation: none;
|
|
853
1020
|
transition: none;
|
|
@@ -518,3 +518,124 @@ describe('ChatView scrolling', () => {
|
|
|
518
518
|
await waitFor(() => { expect(scrollWrites.length).toBe(writesBefore) })
|
|
519
519
|
})
|
|
520
520
|
})
|
|
521
|
+
|
|
522
|
+
describe('ChatView display toggles and context usage', () => {
|
|
523
|
+
/** jsdom in this setup ships a bare localStorage object; install a real fake. */
|
|
524
|
+
const makeStorage = (): Storage => {
|
|
525
|
+
const map = new Map<string, string>()
|
|
526
|
+
return {
|
|
527
|
+
getItem: (key: string) => map.get(key) ?? null,
|
|
528
|
+
setItem: (key: string, value: string) => { map.set(key, value) },
|
|
529
|
+
removeItem: (key: string) => { map.delete(key) },
|
|
530
|
+
clear: () => { map.clear() },
|
|
531
|
+
key: (index: number) => [...map.keys()][index] ?? null,
|
|
532
|
+
get length() { return map.size },
|
|
533
|
+
} as Storage
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/** A user/message whose source.kind is 'plugin' (injected system message). */
|
|
537
|
+
const systemEvents = (): Array<{ event: WireEvent }> => [
|
|
538
|
+
makeEntry('user/message', {
|
|
539
|
+
id: 'u-plugin',
|
|
540
|
+
role: 'user',
|
|
541
|
+
content: [{ type: 'text', text: '系统注入消息' }],
|
|
542
|
+
source: { kind: 'plugin', name: 'react-extension' },
|
|
543
|
+
}, 0),
|
|
544
|
+
makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: '普通消息' }] }, 1),
|
|
545
|
+
]
|
|
546
|
+
|
|
547
|
+
const toolEvents = (): Array<{ event: WireEvent }> => [
|
|
548
|
+
makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: '改文件' }] }, 0),
|
|
549
|
+
makeEntry('assistant/message', {
|
|
550
|
+
turn: 0,
|
|
551
|
+
step: 0,
|
|
552
|
+
message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: '完成' }] },
|
|
553
|
+
}, 1),
|
|
554
|
+
makeEntry('tool/call', { turn: 0, step: 0, callId: 'c1', name: 'bash', arguments: '{}' }, 2),
|
|
555
|
+
]
|
|
556
|
+
|
|
557
|
+
beforeEach(() => {
|
|
558
|
+
vi.stubGlobal('localStorage', makeStorage())
|
|
559
|
+
})
|
|
560
|
+
|
|
561
|
+
afterEach(() => {
|
|
562
|
+
vi.unstubAllGlobals()
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
it('hides injected user messages by default and reveals them via the display sheet', async () => {
|
|
566
|
+
loadHistoryMock.mockResolvedValue(historyPage(systemEvents()))
|
|
567
|
+
render(<ChatView session={session} onBack={() => {}} />)
|
|
568
|
+
|
|
569
|
+
// The plugin-injected message is hidden by default; the real user message shows.
|
|
570
|
+
expect(await screen.findByText('普通消息')).toBeTruthy()
|
|
571
|
+
expect(screen.queryByText('系统注入消息')).toBeNull()
|
|
572
|
+
|
|
573
|
+
// Open the display sheet and flip the system-message switch on.
|
|
574
|
+
fireEvent.click(screen.getByRole('button', { name: /显示/ }))
|
|
575
|
+
const systemSwitch = await screen.findByRole('switch', { name: '系统提示词' })
|
|
576
|
+
expect(systemSwitch.getAttribute('aria-checked')).toBe('false')
|
|
577
|
+
fireEvent.click(systemSwitch)
|
|
578
|
+
expect(await screen.findByText('系统注入消息')).toBeTruthy()
|
|
579
|
+
|
|
580
|
+
// Flip it back off: the injected row disappears again.
|
|
581
|
+
const systemSwitchAfter = screen.getByRole('switch', { name: '系统提示词' })
|
|
582
|
+
expect(systemSwitchAfter.getAttribute('aria-checked')).toBe('true')
|
|
583
|
+
fireEvent.click(systemSwitchAfter)
|
|
584
|
+
expect(screen.queryByText('系统注入消息')).toBeNull()
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('hides the tool disclosure when the tool-call toggle is off', async () => {
|
|
588
|
+
loadHistoryMock.mockResolvedValue(historyPage(toolEvents()))
|
|
589
|
+
render(<ChatView session={session} onBack={() => {}} />)
|
|
590
|
+
|
|
591
|
+
// Tool disclosure visible by default.
|
|
592
|
+
expect(await screen.findByRole('button', { name: /工具/ })).toBeTruthy()
|
|
593
|
+
|
|
594
|
+
fireEvent.click(screen.getByRole('button', { name: /显示/ }))
|
|
595
|
+
const toolSwitch = await screen.findByRole('switch', { name: '工具调用' })
|
|
596
|
+
expect(toolSwitch.getAttribute('aria-checked')).toBe('true')
|
|
597
|
+
fireEvent.click(toolSwitch)
|
|
598
|
+
|
|
599
|
+
// The disclosure is gone while reasoning/text remain.
|
|
600
|
+
expect(screen.queryByRole('button', { name: /工具/ })).toBeNull()
|
|
601
|
+
expect(screen.getByText('完成')).toBeTruthy()
|
|
602
|
+
})
|
|
603
|
+
|
|
604
|
+
it('renders the context usage chip from request/context plus assistant usage', async () => {
|
|
605
|
+
loadHistoryMock.mockResolvedValue(historyPage([
|
|
606
|
+
makeEntry('request/context', { provider: 'fx', model: 'fx-1', contextWindow: 100_000 }, 0),
|
|
607
|
+
makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: 'hi' }] }, 1),
|
|
608
|
+
makeEntry('assistant/message', {
|
|
609
|
+
turn: 0,
|
|
610
|
+
step: 0,
|
|
611
|
+
message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
|
|
612
|
+
usage: { inputTokens: 30_000, outputTokens: 1_000 },
|
|
613
|
+
}, 2),
|
|
614
|
+
]))
|
|
615
|
+
render(<ChatView session={session} onBack={() => {}} />)
|
|
616
|
+
expect(await screen.findByText('上下文 30%')).toBeTruthy()
|
|
617
|
+
})
|
|
618
|
+
|
|
619
|
+
it('adds the warn class when context usage is at or above 80%', async () => {
|
|
620
|
+
loadHistoryMock.mockResolvedValue(historyPage([
|
|
621
|
+
makeEntry('request/context', { provider: 'fx', model: 'fx-1', contextWindow: 100_000 }, 0),
|
|
622
|
+
makeEntry('user/message', { id: 'u-1', role: 'user', content: [{ type: 'text', text: 'hi' }] }, 1),
|
|
623
|
+
makeEntry('assistant/message', {
|
|
624
|
+
turn: 0,
|
|
625
|
+
step: 0,
|
|
626
|
+
message: { id: 'a-1', role: 'assistant', content: [{ type: 'text', text: 'ok' }] },
|
|
627
|
+
usage: { inputTokens: 80_000, outputTokens: 0 },
|
|
628
|
+
}, 2),
|
|
629
|
+
]))
|
|
630
|
+
render(<ChatView session={session} onBack={() => {}} />)
|
|
631
|
+
const chip = await screen.findByText('上下文 80%')
|
|
632
|
+
expect(chip.className).toContain('chat-context-warn')
|
|
633
|
+
})
|
|
634
|
+
|
|
635
|
+
it('renders no context chip when there is no usage/context data', async () => {
|
|
636
|
+
loadHistoryMock.mockResolvedValue(historyPage(turnEvents()))
|
|
637
|
+
render(<ChatView session={session} onBack={() => {}} />)
|
|
638
|
+
await screen.findByText('已完成修改')
|
|
639
|
+
expect(screen.queryByText(/上下文/)).toBeNull()
|
|
640
|
+
})
|
|
641
|
+
})
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
* permission pickers, both as bottom sheets.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { useCallback, useEffect, useRef, useState, type ReactNode } from 'react'
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from 'react'
|
|
15
15
|
import type { MuxFrame } from '@deepseek-ai/dsh-host-apiproxy/api/events'
|
|
16
16
|
import type { SessionModels } from '@deepseek-ai/dsh-host-apiproxy/api/sessions'
|
|
17
17
|
import { loadHistory, prompt, type SessionView } from './App.tsx'
|
|
18
18
|
import { errorText, formatTime, staleHostHint } from './App.tsx'
|
|
19
19
|
import { fetchMobilePreferences, models, selectModel, sendCommand } from '../api.ts'
|
|
20
20
|
import { foldEvents, type RenderMessage, type ToolCallInfo, type WireEvent } from '../messages.ts'
|
|
21
|
+
import { renderMarkdown } from '../markdown.ts'
|
|
21
22
|
import { MuxClient } from '../mux.ts'
|
|
22
23
|
import { ThemeToggle } from '../theme-toggle.tsx'
|
|
23
24
|
|
|
@@ -36,6 +37,31 @@ export interface ChatViewProps {
|
|
|
36
37
|
*/
|
|
37
38
|
export const MAX_TAIL_BUFFER_EVENTS = 500
|
|
38
39
|
|
|
40
|
+
/** localStorage key for the tool-call display toggle (persisted on the /m origin). */
|
|
41
|
+
const SHOW_TOOL_CALLS_KEY = 'dsh.mobile.showToolCalls'
|
|
42
|
+
/** localStorage key for the injected-system-message display toggle. */
|
|
43
|
+
const SHOW_SYSTEM_MESSAGES_KEY = 'dsh.mobile.showSystemMessages'
|
|
44
|
+
|
|
45
|
+
/** Read a boolean from localStorage defensively; falls back to the default. */
|
|
46
|
+
function readStoredBoolean(key: string, fallback: boolean): boolean {
|
|
47
|
+
try {
|
|
48
|
+
const raw = localStorage.getItem(key)
|
|
49
|
+
if (raw === null) return fallback
|
|
50
|
+
return raw === '1' || raw.toLowerCase() === 'true'
|
|
51
|
+
} catch {
|
|
52
|
+
return fallback
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Persist a boolean toggle; storage failures are ignored (feature stays non-persistent). */
|
|
57
|
+
function writeStoredBoolean(key: string, value: boolean): void {
|
|
58
|
+
try {
|
|
59
|
+
localStorage.setItem(key, value ? '1' : '0')
|
|
60
|
+
} catch {
|
|
61
|
+
/* quota / privacy mode: non-persistent is acceptable */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
39
65
|
/** Extract the raw event from one history entry (the fold consumes events only). */
|
|
40
66
|
function eventOf(entry: { event: WireEvent }): WireEvent {
|
|
41
67
|
return entry.event
|
|
@@ -126,7 +152,11 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
126
152
|
/** The current model selection for the toolbar chip (best-effort label). */
|
|
127
153
|
const [currentModel, setCurrentModel] = useState<{ provider: string; model: string; reasoningEffort?: string } | undefined>(undefined)
|
|
128
154
|
/** Which bottom sheet is open. */
|
|
129
|
-
const [sheet, setSheet] = useState<'model' | 'permission' | null>(null)
|
|
155
|
+
const [sheet, setSheet] = useState<'model' | 'permission' | 'display' | null>(null)
|
|
156
|
+
/** Show tool-call disclosures (default on, persisted on the /m origin). */
|
|
157
|
+
const [showToolCalls, setShowToolCalls] = useState<boolean>(() => readStoredBoolean(SHOW_TOOL_CALLS_KEY, true))
|
|
158
|
+
/** Show injected system messages (default off, persisted on the /m origin). */
|
|
159
|
+
const [showSystemMessages, setShowSystemMessages] = useState<boolean>(() => readStoredBoolean(SHOW_SYSTEM_MESSAGES_KEY, false))
|
|
130
160
|
/**
|
|
131
161
|
* Composer preference from the plugin's host settings (default true keeps
|
|
132
162
|
* the legacy Enter-to-send behavior until the preference loads).
|
|
@@ -334,6 +364,23 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
334
364
|
: permissions.options.find(option => option.value === permissions.currentValue)?.name
|
|
335
365
|
?? displayName(permissions.currentValue)
|
|
336
366
|
|
|
367
|
+
// Context usage chip: the most recent assistant message carrying both usage and
|
|
368
|
+
// a positive context window drives the percentage. Scanned from the end so a
|
|
369
|
+
// newer answer (whose usage may be the last one reported) takes precedence.
|
|
370
|
+
const contextUsage = useMemo(() => {
|
|
371
|
+
for (let index = messages.length - 1; index >= 0; index--) {
|
|
372
|
+
const message = messages[index]
|
|
373
|
+
const usage = message.usage
|
|
374
|
+
if (message.kind !== 'assistant' || usage === undefined) continue
|
|
375
|
+
const window = message.contextWindow
|
|
376
|
+
if (window === undefined || window <= 0) continue
|
|
377
|
+
const tokens = usage.inputTokens + (usage.cacheReadTokens ?? 0) + (usage.cacheWriteTokens ?? 0)
|
|
378
|
+
const pct = Math.round(tokens / window * 100)
|
|
379
|
+
return { pct }
|
|
380
|
+
}
|
|
381
|
+
return undefined
|
|
382
|
+
}, [messages])
|
|
383
|
+
|
|
337
384
|
return (
|
|
338
385
|
<div className="chat">
|
|
339
386
|
<header className="mobile-header">
|
|
@@ -348,7 +395,14 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
348
395
|
{loading ? '加载中…' : '加载更早的消息'}
|
|
349
396
|
</button>
|
|
350
397
|
)}
|
|
351
|
-
{messages.map(message =>
|
|
398
|
+
{messages.map(message => (
|
|
399
|
+
<MessageRow
|
|
400
|
+
key={message.id}
|
|
401
|
+
message={message}
|
|
402
|
+
showToolCalls={showToolCalls}
|
|
403
|
+
showSystemMessages={showSystemMessages}
|
|
404
|
+
/>
|
|
405
|
+
))}
|
|
352
406
|
{loading && messages.length === 0 && <p className="chat-typing">加载中…</p>}
|
|
353
407
|
{!loading && messages.length === 0 && <p className="chat-typing">还没有消息,发一句话开始吧</p>}
|
|
354
408
|
</div>
|
|
@@ -358,6 +412,10 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
358
412
|
<span className="chat-chip-value">{modelLabel}</span>
|
|
359
413
|
<span className="chat-chip-chevron" aria-hidden>›</span>
|
|
360
414
|
</button>
|
|
415
|
+
<button type="button" className="chat-chip" onClick={() => { setSheet('display') }} aria-haspopup="dialog">
|
|
416
|
+
<span className="chat-chip-label">显示</span>
|
|
417
|
+
<span className="chat-chip-chevron" aria-hidden>›</span>
|
|
418
|
+
</button>
|
|
361
419
|
{permissionLabel !== undefined && (
|
|
362
420
|
<button type="button" className="chat-chip" onClick={() => { setSheet('permission') }} aria-haspopup="dialog">
|
|
363
421
|
<span className="chat-chip-label">权限</span>
|
|
@@ -365,6 +423,11 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
365
423
|
<span className="chat-chip-chevron" aria-hidden>›</span>
|
|
366
424
|
</button>
|
|
367
425
|
)}
|
|
426
|
+
{contextUsage !== undefined && (
|
|
427
|
+
<div className={"chat-context" + (contextUsage.pct >= 80 ? " chat-context-warn" : "")}>
|
|
428
|
+
上下文 {contextUsage.pct}%
|
|
429
|
+
</div>
|
|
430
|
+
)}
|
|
368
431
|
</div>
|
|
369
432
|
<div className="chat-inputbar">
|
|
370
433
|
<textarea
|
|
@@ -403,6 +466,15 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
403
466
|
onClose={() => { setSheet(null) }}
|
|
404
467
|
/>
|
|
405
468
|
)}
|
|
469
|
+
{sheet === 'display' && (
|
|
470
|
+
<DisplaySheet
|
|
471
|
+
showToolCalls={showToolCalls}
|
|
472
|
+
showSystemMessages={showSystemMessages}
|
|
473
|
+
onToolCalls={(value) => { setShowToolCalls(value); writeStoredBoolean(SHOW_TOOL_CALLS_KEY, value) }}
|
|
474
|
+
onSystemMessages={(value) => { setShowSystemMessages(value); writeStoredBoolean(SHOW_SYSTEM_MESSAGES_KEY, value) }}
|
|
475
|
+
onClose={() => { setSheet(null) }}
|
|
476
|
+
/>
|
|
477
|
+
)}
|
|
406
478
|
</div>
|
|
407
479
|
)
|
|
408
480
|
}
|
|
@@ -410,16 +482,30 @@ export function ChatView({ session, mux, onBack }: ChatViewProps) {
|
|
|
410
482
|
/* ── message rows ─────────────────────────────────────────────────────── */
|
|
411
483
|
|
|
412
484
|
/** One rendered message row (user bubble or assistant bubble with folds). */
|
|
413
|
-
function MessageRow({ message
|
|
485
|
+
function MessageRow({ message, showToolCalls, showSystemMessages }: {
|
|
486
|
+
message: RenderMessage
|
|
487
|
+
showToolCalls: boolean
|
|
488
|
+
showSystemMessages: boolean
|
|
489
|
+
}) {
|
|
490
|
+
// Injected user messages (sourceKind defined and not 'user') hide behind
|
|
491
|
+
// the system-message toggle. Assistant messages are never hidden.
|
|
492
|
+
if (message.kind === 'user'
|
|
493
|
+
&& message.sourceKind !== undefined
|
|
494
|
+
&& message.sourceKind !== 'user'
|
|
495
|
+
&& !showSystemMessages) {
|
|
496
|
+
return null
|
|
497
|
+
}
|
|
414
498
|
return (
|
|
415
499
|
<div className={`chat-msg chat-msg-${message.kind}${message.pending === true ? ' chat-msg-pending' : ''}${message.failed === true ? ' chat-msg-failed' : ''}`}>
|
|
416
500
|
{message.kind === 'assistant' && message.reasoning !== undefined && message.reasoning !== '' && (
|
|
417
501
|
<ReasoningDisclosure text={message.reasoning} pending={message.pending === true} />
|
|
418
502
|
)}
|
|
419
|
-
{message.kind === 'assistant' && message.tools !== undefined && message.tools.length > 0 && (
|
|
503
|
+
{showToolCalls && message.kind === 'assistant' && message.tools !== undefined && message.tools.length > 0 && (
|
|
420
504
|
<ToolDisclosure tools={message.tools} />
|
|
421
505
|
)}
|
|
422
|
-
|
|
506
|
+
{message.kind === 'assistant'
|
|
507
|
+
? <MarkdownText text={message.text} />
|
|
508
|
+
: <CollapsibleText text={message.text} />}
|
|
423
509
|
{message.failed === true && <span className="chat-msg-failtag">本次回复失败</span>}
|
|
424
510
|
<span className="chat-msg-time">{formatTime(message.time)}</span>
|
|
425
511
|
</div>
|
|
@@ -478,6 +564,30 @@ function ToolDisclosure({ tools }: { tools: ToolCallInfo[] }) {
|
|
|
478
564
|
)
|
|
479
565
|
}
|
|
480
566
|
|
|
567
|
+
/**
|
|
568
|
+
* Assistant text rendered as GFM markdown (escape-first, protocol
|
|
569
|
+
* allow-list — see markdown.ts). Long replies collapse by clamping the
|
|
570
|
+
* rendered block height instead of slicing the source, so half-cut code
|
|
571
|
+
* fences or tables never leak malformed markup into the DOM. User
|
|
572
|
+
* messages stay plain text (CollapsibleText).
|
|
573
|
+
*/
|
|
574
|
+
function MarkdownText({ text }: { text: string }) {
|
|
575
|
+
const [open, setOpen] = useState(false)
|
|
576
|
+
const html = useMemo(() => renderMarkdown(text), [text])
|
|
577
|
+
const long = text.length > LONG_TEXT_LIMIT
|
|
578
|
+
const collapsed = long && !open
|
|
579
|
+
return (
|
|
580
|
+
<div className={'chat-msg-text chat-md' + (collapsed ? ' chat-md-collapsed' : '')}>
|
|
581
|
+
<div className="chat-md-body" dangerouslySetInnerHTML={{ __html: html }} />
|
|
582
|
+
{long && (
|
|
583
|
+
<button type="button" className="chat-msg-toggle" onClick={() => { setOpen(value => !value) }}>
|
|
584
|
+
{open ? '收起' : '展开全文(' + text.length + ' 字)'}
|
|
585
|
+
</button>
|
|
586
|
+
)}
|
|
587
|
+
</div>
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
|
|
481
591
|
/** Long assistant text collapses behind an explicit expand toggle. */
|
|
482
592
|
function CollapsibleText({ text }: { text: string }) {
|
|
483
593
|
const [open, setOpen] = useState(false)
|
|
@@ -756,3 +866,51 @@ function PermissionSheet({ sessionId, value, onChanged, onClose }: {
|
|
|
756
866
|
</Sheet>
|
|
757
867
|
)
|
|
758
868
|
}
|
|
869
|
+
|
|
870
|
+
/** The display-options sheet: tool calls and injected system messages toggles. */
|
|
871
|
+
function DisplaySheet({ showToolCalls, showSystemMessages, onToolCalls, onSystemMessages, onClose }: {
|
|
872
|
+
showToolCalls: boolean
|
|
873
|
+
showSystemMessages: boolean
|
|
874
|
+
onToolCalls(value: boolean): void
|
|
875
|
+
onSystemMessages(value: boolean): void
|
|
876
|
+
onClose(): void
|
|
877
|
+
}) {
|
|
878
|
+
return (
|
|
879
|
+
<Sheet title="显示" onClose={onClose}>
|
|
880
|
+
<div role="group" aria-label="显示选项">
|
|
881
|
+
<div className="sheet-toggle-row">
|
|
882
|
+
<div className="sheet-toggle-copy">
|
|
883
|
+
<span className="sheet-toggle-title">工具调用</span>
|
|
884
|
+
<span className="sheet-toggle-desc">显示助手使用的工具调用</span>
|
|
885
|
+
</div>
|
|
886
|
+
<button
|
|
887
|
+
type="button"
|
|
888
|
+
role="switch"
|
|
889
|
+
aria-label="工具调用"
|
|
890
|
+
aria-checked={showToolCalls}
|
|
891
|
+
className={`sheet-toggle-switch${showToolCalls ? ' sheet-toggle-switch-on' : ''}`}
|
|
892
|
+
onClick={() => { onToolCalls(!showToolCalls) }}
|
|
893
|
+
>
|
|
894
|
+
<span className="sheet-toggle-switch-knob" aria-hidden />
|
|
895
|
+
</button>
|
|
896
|
+
</div>
|
|
897
|
+
<div className="sheet-toggle-row">
|
|
898
|
+
<div className="sheet-toggle-copy">
|
|
899
|
+
<span className="sheet-toggle-title">系统提示词</span>
|
|
900
|
+
<span className="sheet-toggle-desc">显示注入到对话中的系统消息</span>
|
|
901
|
+
</div>
|
|
902
|
+
<button
|
|
903
|
+
type="button"
|
|
904
|
+
role="switch"
|
|
905
|
+
aria-label="系统提示词"
|
|
906
|
+
aria-checked={showSystemMessages}
|
|
907
|
+
className={`sheet-toggle-switch${showSystemMessages ? ' sheet-toggle-switch-on' : ''}`}
|
|
908
|
+
onClick={() => { onSystemMessages(!showSystemMessages) }}
|
|
909
|
+
>
|
|
910
|
+
<span className="sheet-toggle-switch-knob" aria-hidden />
|
|
911
|
+
</button>
|
|
912
|
+
</div>
|
|
913
|
+
</div>
|
|
914
|
+
</Sheet>
|
|
915
|
+
)
|
|
916
|
+
}
|