@linxin666/dsh-remote-web-ui 0.2.8 → 0.3.0

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.
Files changed (86) hide show
  1. package/LICENSE +202 -28
  2. package/README.i18n.yaml +2 -2
  3. package/README.md +51 -15
  4. package/README.zh.md +9 -4
  5. package/lib/client.js +65 -34
  6. package/lib/client.js.map +1 -1
  7. package/lib/index.js +667 -77
  8. package/lib/mobile.js +874 -64
  9. package/lib/mobile.js.map +1 -1
  10. package/lib/types/client/index.d.ts.map +1 -1
  11. package/lib/types/client/index.js +35 -5
  12. package/lib/types/client/remote-channel.d.ts +4 -5
  13. package/lib/types/client/remote-channel.d.ts.map +1 -1
  14. package/lib/types/client/remote-channel.js +13 -28
  15. package/lib/types/http.d.ts +37 -8
  16. package/lib/types/http.d.ts.map +1 -1
  17. package/lib/types/http.js +76 -11
  18. package/lib/types/index.d.ts.map +1 -1
  19. package/lib/types/index.js +40 -5
  20. package/lib/types/mobile/api.d.ts +57 -0
  21. package/lib/types/mobile/api.d.ts.map +1 -1
  22. package/lib/types/mobile/api.js +29 -0
  23. package/lib/types/mobile/markdown.d.ts +7 -1
  24. package/lib/types/mobile/markdown.d.ts.map +1 -1
  25. package/lib/types/mobile/markdown.js +16 -7
  26. package/lib/types/mobile/markdown.test.js +8 -0
  27. package/lib/types/mobile/mobile-styles.d.ts +1 -1
  28. package/lib/types/mobile/mobile-styles.d.ts.map +1 -1
  29. package/lib/types/mobile/mobile-styles.js +325 -6
  30. package/lib/types/mobile/views/ChatView.d.ts +10 -0
  31. package/lib/types/mobile/views/ChatView.d.ts.map +1 -1
  32. package/lib/types/mobile/views/ChatView.js +205 -8
  33. package/lib/types/mobile/views/ChatView.stream.test.d.ts +2 -0
  34. package/lib/types/mobile/views/ChatView.stream.test.d.ts.map +1 -0
  35. package/lib/types/mobile/views/ChatView.stream.test.js +205 -0
  36. package/lib/types/mobile/views/WorkspaceView.d.ts.map +1 -1
  37. package/lib/types/mobile/views/WorkspaceView.js +46 -5
  38. package/lib/types/mobile/views/WorkspaceView.test.js +129 -2
  39. package/lib/types/mobile-api.d.ts +9 -3
  40. package/lib/types/mobile-api.d.ts.map +1 -1
  41. package/lib/types/mobile-api.js +61 -16
  42. package/lib/types/mobile-pending.d.ts +38 -0
  43. package/lib/types/mobile-pending.d.ts.map +1 -0
  44. package/lib/types/mobile-pending.js +59 -0
  45. package/lib/types/paired-model-catalog.d.ts +17 -0
  46. package/lib/types/paired-model-catalog.d.ts.map +1 -0
  47. package/lib/types/paired-model-catalog.js +252 -0
  48. package/lib/types/pairing.d.ts +13 -4
  49. package/lib/types/pairing.d.ts.map +1 -1
  50. package/lib/types/pairing.js +13 -5
  51. package/lib/types/remote-api.d.ts +13 -4
  52. package/lib/types/remote-api.d.ts.map +1 -1
  53. package/lib/types/remote-api.js +27 -17
  54. package/lib/types/remote-channel-boot.d.ts +26 -0
  55. package/lib/types/remote-channel-boot.d.ts.map +1 -0
  56. package/lib/types/remote-channel-boot.js +128 -0
  57. package/lib/types/remote-channel-rules.d.ts +40 -0
  58. package/lib/types/remote-channel-rules.d.ts.map +1 -0
  59. package/lib/types/remote-channel-rules.js +32 -0
  60. package/lib/types/routes.d.ts.map +1 -1
  61. package/lib/types/routes.js +12 -23
  62. package/lib/types/uuid-polyfill.d.ts +10 -0
  63. package/lib/types/uuid-polyfill.d.ts.map +1 -0
  64. package/lib/types/uuid-polyfill.js +27 -0
  65. package/package.json +1 -1
  66. package/src/client/index.ts +41 -5
  67. package/src/client/remote-channel.ts +18 -28
  68. package/src/http.ts +87 -12
  69. package/src/index.ts +42 -6
  70. package/src/mobile/api.ts +91 -0
  71. package/src/mobile/markdown.test.ts +9 -0
  72. package/src/mobile/markdown.ts +17 -7
  73. package/src/mobile/mobile-styles.ts +326 -6
  74. package/src/mobile/views/ChatView.stream.test.tsx +231 -0
  75. package/src/mobile/views/ChatView.tsx +328 -10
  76. package/src/mobile/views/WorkspaceView.test.tsx +163 -2
  77. package/src/mobile/views/WorkspaceView.tsx +152 -26
  78. package/src/mobile-api.ts +59 -17
  79. package/src/mobile-pending.ts +88 -0
  80. package/src/paired-model-catalog.ts +224 -0
  81. package/src/pairing.ts +13 -5
  82. package/src/remote-api.ts +34 -18
  83. package/src/remote-channel-boot.ts +131 -0
  84. package/src/remote-channel-rules.ts +62 -0
  85. package/src/routes.ts +13 -24
  86. package/src/uuid-polyfill.ts +28 -0
package/src/mobile/api.ts CHANGED
@@ -129,3 +129,94 @@ export async function selectModel(
129
129
  ...(selection.reasoningEffort !== undefined ? { reasoningEffort: selection.reasoningEffort } : {}),
130
130
  })
131
131
  }
132
+
133
+ /* ── pending approvals / questions (#1025) ───────────────────────────── */
134
+
135
+ /** One pending tool approval awaiting the user's decision. */
136
+ export interface PendingApproval {
137
+ approvalId: string
138
+ toolName: string
139
+ callId?: string
140
+ reason?: string
141
+ }
142
+
143
+ /** One pending question group awaiting the user's answer. */
144
+ export interface PendingQuestionItem {
145
+ id: string
146
+ question: string
147
+ detail?: string
148
+ header?: string
149
+ options?: Array<{ label: string; description?: string }>
150
+ multiSelect?: boolean
151
+ }
152
+
153
+ /** The pending state for one session. */
154
+ export interface PendingState {
155
+ approvals: PendingApproval[]
156
+ questions: PendingQuestionItem[]
157
+ }
158
+
159
+ /** Fetch pending approvals and questions for one session (polling fallback data source). */
160
+ export async function fetchPending(sessionId: string): Promise<PendingState> {
161
+ return await callUnary<PendingState>('mobile.pending', { sessionId })
162
+ }
163
+
164
+ /** Submit an approval decision (allowed-once or rejected). */
165
+ export async function respondApproval(
166
+ sessionId: string,
167
+ approvalId: string,
168
+ outcome: 'allowed-once' | 'rejected',
169
+ ): Promise<void> {
170
+ await callUnary<unknown>('mobile.respond', {
171
+ sessionId,
172
+ type: 'approval',
173
+ approvalId,
174
+ outcome,
175
+ })
176
+ }
177
+
178
+ /** Submit answers to a question group. */
179
+ export async function respondQuestion(
180
+ sessionId: string,
181
+ answers: Array<{ id: string; selected: string[]; custom?: string }>,
182
+ ): Promise<void> {
183
+ await callUnary<unknown>('mobile.respond', {
184
+ sessionId,
185
+ type: 'question',
186
+ answers,
187
+ })
188
+ }
189
+
190
+ /* ── directory browsing / workspace creation (#977) ──────────────────── */
191
+
192
+ /** One entry in a directory listing. */
193
+ export interface DirectoryEntry {
194
+ name: string
195
+ path: string
196
+ hidden: boolean
197
+ }
198
+
199
+ /** The host directory listing result (one level, with ancestor breadcrumbs). */
200
+ export interface DirectoryListing {
201
+ path: string
202
+ home: string
203
+ crumbs: DirectoryEntry[]
204
+ entries: DirectoryEntry[]
205
+ truncated: boolean
206
+ }
207
+
208
+ /** The result of creating a workspace from an existing directory. */
209
+ export interface WorkspaceCreateResult {
210
+ workspace: WorkspaceView
211
+ created: boolean
212
+ }
213
+
214
+ /** Browse one directory level on the host (defaults to home directory). */
215
+ export async function listDirectory(path?: string): Promise<DirectoryListing> {
216
+ return await callUnary<DirectoryListing>('host.listDirectory', path === undefined ? {} : { path })
217
+ }
218
+
219
+ /** Create a workspace from an existing host directory (does not mkdir). */
220
+ export async function createWorkspace(path: string): Promise<WorkspaceCreateResult> {
221
+ return await callUnary<WorkspaceCreateResult>('workspace.create', { path })
222
+ }
@@ -79,4 +79,13 @@ describe('escapeHtml', () => {
79
79
  it('escapes all five HTML-significant characters', () => {
80
80
  expect(escapeHtml('<&>"\'')).toBe('&lt;&amp;&gt;&quot;&#39;')
81
81
  })
82
+
83
+ it('equals the five sequential passes on mixed, pre-escaped and plain input', () => {
84
+ // Single-pass map replacement must never diverge from the sequential
85
+ // five-pass escape (including double-escaping of literal entities).
86
+ expect(escapeHtml('a & b < c > d " e \' f')).toBe('a &amp; b &lt; c &gt; d &quot; e &#39; f')
87
+ expect(escapeHtml('&amp; &lt; &gt;')).toBe('&amp;amp; &amp;lt; &amp;gt;')
88
+ expect(escapeHtml('plain text 123')).toBe('plain text 123')
89
+ expect(escapeHtml('')).toBe('')
90
+ })
82
91
  })
@@ -10,14 +10,24 @@
10
10
  * @module dsh-remote-web-ui/mobile/markdown
11
11
  */
12
12
 
13
- /** Escape HTML special characters. */
13
+ /** HTML special-character map for {@link escapeHtml}. */
14
+ const HTML_ESCAPE_MAP: Record<string, string> = {
15
+ '&': '&amp;',
16
+ '<': '&lt;',
17
+ '>': '&gt;',
18
+ '"': '&quot;',
19
+ "'": '&#39;',
20
+ }
21
+
22
+ /**
23
+ * Escape HTML special characters. One regex pass replaces all five
24
+ * characters through the map; the output is identical to five sequential
25
+ * passes (each replacement string contains none of the escaped characters),
26
+ * and the common no-special-character case scans the string only once
27
+ * instead of five times.
28
+ */
14
29
  export function escapeHtml(text: string): string {
15
- return text
16
- .replace(/&/g, '&amp;')
17
- .replace(/</g, '&lt;')
18
- .replace(/>/g, '&gt;')
19
- .replace(/"/g, '&quot;')
20
- .replace(/'/g, '&#39;')
30
+ return text.replace(/[&<>"']/g, char => HTML_ESCAPE_MAP[char] ?? char)
21
31
  }
22
32
 
23
33
  /**
@@ -588,16 +588,33 @@ body {
588
588
  gap: 8px;
589
589
  }
590
590
 
591
- .chat-tool-item {
591
+ .chat-tool-card {
592
592
  display: flex;
593
593
  flex-direction: column;
594
- gap: 2px;
594
+ gap: 4px;
595
+ padding: 6px 8px;
596
+ border: 1px solid var(--m-border);
597
+ border-radius: 8px;
598
+ background: var(--m-bg-raised);
595
599
  }
596
600
 
597
- .chat-tool-name {
598
- color: var(--m-text);
599
- font-size: 12.5px;
601
+ .chat-tool-pills {
602
+ display: flex;
603
+ gap: 4px;
604
+ flex-wrap: wrap;
605
+ align-items: center;
606
+ }
607
+
608
+ .chat-tool-pill {
609
+ display: inline-block;
610
+ padding: 1px 7px;
611
+ border-radius: 999px;
612
+ background: var(--m-accent-soft);
613
+ color: var(--m-accent);
614
+ font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
615
+ font-size: 11px;
600
616
  font-weight: 600;
617
+ white-space: nowrap;
601
618
  }
602
619
 
603
620
  .chat-tool-args {
@@ -611,8 +628,245 @@ body {
611
628
  line-height: 1.5;
612
629
  overflow-wrap: break-word;
613
630
  white-space: pre-wrap;
631
+ max-height: 200px;
632
+ overflow-y: auto;
633
+ }
634
+
635
+ /* ── turn status indicator (#1017) ───────────────────────────────────── */
636
+
637
+ .chat-turn-status {
638
+ display: flex;
639
+ align-items: center;
640
+ gap: 6px;
641
+ padding: 10px 16px;
642
+ color: var(--m-accent);
643
+ font-size: 13px;
644
+ font-weight: 600;
645
+ background: linear-gradient(
646
+ 90deg,
647
+ var(--m-accent) 0%,
648
+ color-mix(in srgb, var(--m-accent) 60%, #ffffff) 50%,
649
+ var(--m-accent) 100%
650
+ );
651
+ background-size: 200% 100%;
652
+ background-clip: text;
653
+ -webkit-background-clip: text;
654
+ -webkit-text-fill-color: transparent;
655
+ animation: dsh-shimmer 1.8s linear infinite;
656
+ }
657
+
658
+ .chat-turn-dots {
659
+ display: inline-flex;
660
+ gap: 3px;
661
+ }
662
+
663
+ .chat-turn-dots span {
664
+ display: inline-block;
665
+ width: 5px;
666
+ height: 5px;
667
+ border-radius: 50%;
668
+ background: var(--m-accent);
669
+ animation: dsh-dot-bounce 1.2s ease-in-out infinite;
670
+ }
671
+
672
+ .chat-turn-dots span:nth-child(2) {
673
+ animation-delay: 0.15s;
674
+ }
675
+
676
+ .chat-turn-dots span:nth-child(3) {
677
+ animation-delay: 0.3s;
678
+ }
679
+
680
+ @keyframes dsh-shimmer {
681
+ from { background-position: 200% 0; }
682
+ to { background-position: -200% 0; }
683
+ }
684
+
685
+ @keyframes dsh-dot-bounce {
686
+ 0%, 80%, 100% { transform: translateY(0); opacity: 0.4; }
687
+ 40% { transform: translateY(-4px); opacity: 1; }
688
+ }
689
+
690
+ /* ── approval panel (#1025) ──────────────────────────────────────────── */
691
+
692
+ .chat-approval-panel {
693
+ margin: 8px 12px;
694
+ padding: 10px 12px;
695
+ border: 1px solid var(--m-accent);
696
+ border-radius: var(--m-radius);
697
+ background: var(--m-bg-raised);
698
+ box-shadow: var(--m-shadow);
699
+ }
700
+
701
+ .chat-approval-header {
702
+ display: flex;
703
+ align-items: center;
704
+ gap: 8px;
705
+ margin-bottom: 8px;
614
706
  }
615
707
 
708
+ .chat-approval-reason {
709
+ color: var(--m-text-secondary);
710
+ font-size: 12.5px;
711
+ }
712
+
713
+ .chat-approval-error {
714
+ margin: 4px 0;
715
+ color: var(--m-danger);
716
+ font-size: 12px;
717
+ }
718
+
719
+ .chat-approval-actions {
720
+ display: flex;
721
+ gap: 8px;
722
+ }
723
+
724
+ .chat-approval-allow {
725
+ flex: 1;
726
+ height: 34px;
727
+ border: none;
728
+ border-radius: 8px;
729
+ background: var(--m-accent);
730
+ color: #fff;
731
+ font: inherit;
732
+ font-size: 13px;
733
+ font-weight: 600;
734
+ cursor: pointer;
735
+ }
736
+
737
+ .chat-approval-allow:disabled {
738
+ opacity: 0.6;
739
+ }
740
+
741
+ .chat-approval-reject {
742
+ flex: 1;
743
+ height: 34px;
744
+ border: 1px solid var(--m-border);
745
+ border-radius: 8px;
746
+ background: var(--m-bg-raised);
747
+ color: var(--m-text);
748
+ font: inherit;
749
+ font-size: 13px;
750
+ cursor: pointer;
751
+ }
752
+
753
+ .chat-approval-reject:disabled {
754
+ opacity: 0.6;
755
+ }
756
+
757
+ /* ── question panel (#1025) ──────────────────────────────────────────── */
758
+
759
+ .chat-question-panel {
760
+ margin: 8px 12px;
761
+ padding: 10px 12px;
762
+ border: 1px solid var(--m-accent);
763
+ border-radius: var(--m-radius);
764
+ background: var(--m-bg-raised);
765
+ box-shadow: var(--m-shadow);
766
+ display: flex;
767
+ flex-direction: column;
768
+ gap: 10px;
769
+ }
770
+
771
+ .chat-question-group {
772
+ display: flex;
773
+ flex-direction: column;
774
+ gap: 6px;
775
+ }
776
+
777
+ .chat-question-header {
778
+ font-size: 14px;
779
+ font-weight: 600;
780
+ color: var(--m-text);
781
+ }
782
+
783
+ .chat-question-text {
784
+ font-size: 13px;
785
+ color: var(--m-text);
786
+ line-height: 1.5;
787
+ }
788
+
789
+ .chat-question-detail {
790
+ font-size: 12px;
791
+ color: var(--m-text-secondary);
792
+ line-height: 1.5;
793
+ }
794
+
795
+ .chat-question-options {
796
+ display: flex;
797
+ flex-direction: column;
798
+ gap: 4px;
799
+ }
800
+
801
+ .chat-question-option {
802
+ display: flex;
803
+ align-items: flex-start;
804
+ gap: 8px;
805
+ padding: 8px 10px;
806
+ border: 1px solid var(--m-border);
807
+ border-radius: 8px;
808
+ cursor: pointer;
809
+ font-size: 13px;
810
+ transition: border-color 0.12s, background 0.12s;
811
+ }
812
+
813
+ .chat-question-option input {
814
+ margin: 2px 0 0;
815
+ flex: none;
816
+ }
817
+
818
+ .chat-question-option-selected {
819
+ border-color: var(--m-accent);
820
+ background: var(--m-accent-soft);
821
+ }
822
+
823
+ .chat-question-option-label {
824
+ color: var(--m-text);
825
+ font-weight: 500;
826
+ }
827
+
828
+ .chat-question-option-desc {
829
+ display: block;
830
+ color: var(--m-text-tertiary);
831
+ font-size: 12px;
832
+ }
833
+
834
+ .chat-question-custom {
835
+ width: 100%;
836
+ min-height: 40px;
837
+ box-sizing: border-box;
838
+ padding: 8px 10px;
839
+ border: 1px solid var(--m-border);
840
+ border-radius: 8px;
841
+ background: var(--m-bg-input);
842
+ color: var(--m-text);
843
+ font: inherit;
844
+ font-size: 13px;
845
+ resize: vertical;
846
+ outline: none;
847
+ }
848
+
849
+ .chat-question-custom:focus-visible {
850
+ border-color: var(--m-accent);
851
+ }
852
+
853
+ .chat-question-submit {
854
+ height: 36px;
855
+ border: none;
856
+ border-radius: 8px;
857
+ background: var(--m-accent);
858
+ color: #fff;
859
+ font: inherit;
860
+ font-size: 13px;
861
+ font-weight: 600;
862
+ cursor: pointer;
863
+ }
864
+
865
+ .chat-question-submit:disabled {
866
+ opacity: 0.6;
867
+ }
868
+
869
+
616
870
  /* ── composer toolbar (model / permission chips) ─────────────────────── */
617
871
 
618
872
  .chat-tools {
@@ -1053,10 +1307,16 @@ body {
1053
1307
  .sheet-confirm-danger,
1054
1308
  .sheet-toggle-switch,
1055
1309
  .sheet-toggle-switch-knob,
1056
- .chat-input {
1310
+ .chat-input,
1311
+ .chat-turn-status,
1312
+ .chat-turn-dots span {
1057
1313
  animation: none;
1058
1314
  transition: none;
1059
1315
  }
1316
+ .chat-turn-status {
1317
+ background: none;
1318
+ -webkit-text-fill-color: var(--m-accent);
1319
+ }
1060
1320
  }
1061
1321
 
1062
1322
  /* ── installed-app pairing ───────────────────────────────────────────── */
@@ -1106,4 +1366,64 @@ body {
1106
1366
  .mobile-pairSubmit {
1107
1367
  width: 100%;
1108
1368
  }
1369
+
1370
+ /* ── directory browser ─────────────────────────────────────────────── */
1371
+
1372
+ .dir-browser {
1373
+ display: flex;
1374
+ flex-direction: column;
1375
+ }
1376
+
1377
+ .dir-crumbs {
1378
+ display: flex;
1379
+ align-items: center;
1380
+ overflow-x: auto;
1381
+ padding: 12px 16px;
1382
+ background: var(--m-bg-raised);
1383
+ border-bottom: 1px solid var(--m-border);
1384
+ white-space: nowrap;
1385
+ }
1386
+
1387
+ .dir-crumbs::-webkit-scrollbar {
1388
+ display: none;
1389
+ }
1390
+
1391
+ .dir-crumb {
1392
+ border: none;
1393
+ background: transparent;
1394
+ padding: 4px 6px;
1395
+ color: var(--m-text);
1396
+ font-size: 14px;
1397
+ cursor: pointer;
1398
+ }
1399
+
1400
+ .dir-crumb:active {
1401
+ background: var(--m-bg-input);
1402
+ border-radius: 4px;
1403
+ }
1404
+
1405
+ .dir-crumb-separator {
1406
+ color: var(--m-text-tertiary);
1407
+ margin: 0 4px;
1408
+ font-size: 14px;
1409
+ }
1410
+
1411
+ .dir-entry {
1412
+ padding-left: 16px;
1413
+ }
1414
+
1415
+ .dir-entry-hidden {
1416
+ opacity: 0.5;
1417
+ }
1418
+
1419
+ .dir-select {
1420
+ padding: 16px;
1421
+ background: var(--m-bg);
1422
+ border-top: 1px solid var(--m-border);
1423
+ }
1424
+
1425
+ .dir-empty {
1426
+ padding: 40px 0;
1427
+ }
1109
1428
  `
1429
+