@deepseek-harness-tui/dsh-tui 0.11.2 → 0.12.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 (168) hide show
  1. package/README.md +21 -5
  2. package/bin/dsh-tui.js +30 -3
  3. package/cordis.patch.yml +24 -5
  4. package/guide/dsh-tui-guide/SKILL.md +37 -0
  5. package/guide/dsh-tui-guide/configuration.en.md +410 -0
  6. package/guide/dsh-tui-guide/configuration.md +349 -0
  7. package/guide/dsh-tui-guide/getting-started.en.md +398 -0
  8. package/guide/dsh-tui-guide/getting-started.md +397 -0
  9. package/guide/dsh-tui-guide/interaction.en.md +674 -0
  10. package/guide/dsh-tui-guide/interaction.md +747 -0
  11. package/guide/dsh-tui-guide/migrate.en.md +166 -0
  12. package/guide/dsh-tui-guide/migrate.md +132 -0
  13. package/guide/dsh-tui-guide/plugins.en.md +68 -0
  14. package/guide/dsh-tui-guide/plugins.md +56 -0
  15. package/guide/dsh-tui-guide/themes.en.md +192 -0
  16. package/guide/dsh-tui-guide/themes.md +185 -0
  17. package/guide/dsh-tui-guide/user-guide.en.md +608 -0
  18. package/guide/dsh-tui-guide/user-guide.md +586 -0
  19. package/guide/dsh-tui-guide/vscode.en.md +301 -0
  20. package/guide/dsh-tui-guide/vscode.md +272 -0
  21. package/lib/settings.json +5 -5
  22. package/lib/types/adapter/channel/ui-policy.d.ts +3 -2
  23. package/lib/types/adapter/channel/ui-policy.d.ts.map +1 -1
  24. package/lib/types/adapter/channel/ui-policy.js +4 -0
  25. package/lib/types/adapter/ports/channel-settings.d.ts +2 -1
  26. package/lib/types/adapter/ports/channel-settings.d.ts.map +1 -1
  27. package/lib/types/adapter/ports/channel-ui.d.ts +29 -2
  28. package/lib/types/adapter/ports/channel-ui.d.ts.map +1 -1
  29. package/lib/types/adapter/ports/channel-view.d.ts +43 -9
  30. package/lib/types/adapter/ports/channel-view.d.ts.map +1 -1
  31. package/lib/types/components/Chat/JobCard.d.ts +9 -0
  32. package/lib/types/components/Chat/JobCard.d.ts.map +1 -1
  33. package/lib/types/components/Chat/JobCard.js +42 -15
  34. package/lib/types/components/Chat/SubagentMessage.js +6 -6
  35. package/lib/types/components/JobsPanel.d.ts.map +1 -1
  36. package/lib/types/components/JobsPanel.js +80 -33
  37. package/lib/types/components/MessageList.js +3 -3
  38. package/lib/types/components/PresetPicker.d.ts +4 -0
  39. package/lib/types/components/PresetPicker.d.ts.map +1 -1
  40. package/lib/types/components/PresetPicker.js +5 -1
  41. package/lib/types/components/PromptInput.d.ts.map +1 -1
  42. package/lib/types/components/PromptInput.js +12 -1
  43. package/lib/types/components/StarPrompt.d.ts +6 -0
  44. package/lib/types/components/StarPrompt.d.ts.map +1 -1
  45. package/lib/types/components/StarPrompt.js +45 -0
  46. package/lib/types/components/SubagentCard.d.ts.map +1 -1
  47. package/lib/types/components/SubagentCard.js +8 -8
  48. package/lib/types/components/SubagentDetailScene.d.ts.map +1 -1
  49. package/lib/types/components/SubagentDetailScene.js +108 -10
  50. package/lib/types/components/WhaleGirl.d.ts +6 -0
  51. package/lib/types/components/WhaleGirl.d.ts.map +1 -1
  52. package/lib/types/components/WhaleGirl.js +18 -0
  53. package/lib/types/components/messages/AssistantThinkingMessage.js +4 -4
  54. package/lib/types/components/trajectory/MiniWake.d.ts +11 -1
  55. package/lib/types/components/trajectory/MiniWake.d.ts.map +1 -1
  56. package/lib/types/components/trajectory/MiniWake.js +9 -3
  57. package/lib/types/dsh-adapter/channel/external-commands.d.ts.map +1 -1
  58. package/lib/types/dsh-adapter/channel/external-commands.js +8 -2
  59. package/lib/types/dsh-adapter/channel/job-projection.d.ts +1 -0
  60. package/lib/types/dsh-adapter/channel/job-projection.d.ts.map +1 -1
  61. package/lib/types/dsh-adapter/channel/job-projection.js +88 -3
  62. package/lib/types/dsh-adapter/channel/mode-actions.d.ts +13 -0
  63. package/lib/types/dsh-adapter/channel/mode-actions.d.ts.map +1 -1
  64. package/lib/types/dsh-adapter/channel/mode-actions.js +48 -7
  65. package/lib/types/dsh-adapter/channel/mode-permission-actions.d.ts.map +1 -1
  66. package/lib/types/dsh-adapter/channel/mode-permission-actions.js +25 -9
  67. package/lib/types/dsh-adapter/channel/preferences.d.ts +1 -1
  68. package/lib/types/dsh-adapter/channel/preferences.d.ts.map +1 -1
  69. package/lib/types/dsh-adapter/channel/preferences.js +17 -7
  70. package/lib/types/dsh-adapter/channel/state.d.ts +4 -2
  71. package/lib/types/dsh-adapter/channel/state.d.ts.map +1 -1
  72. package/lib/types/dsh-adapter/channel/state.js +1 -1
  73. package/lib/types/dsh-adapter/channel/subagent-projection.d.ts.map +1 -1
  74. package/lib/types/dsh-adapter/channel/subagent-projection.js +5 -0
  75. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  76. package/lib/types/dsh-adapter/channel.js +16 -1
  77. package/lib/types/dsh-adapter/contract.d.ts +5 -4
  78. package/lib/types/dsh-adapter/contract.d.ts.map +1 -1
  79. package/lib/types/dsh-adapter/contract.js +9 -3
  80. package/lib/types/dsh-adapter/jobs.d.ts +82 -16
  81. package/lib/types/dsh-adapter/jobs.d.ts.map +1 -1
  82. package/lib/types/dsh-adapter/jobs.js +104 -9
  83. package/lib/types/dsh-adapter/oauth/bonus.d.ts +27 -0
  84. package/lib/types/dsh-adapter/oauth/bonus.d.ts.map +1 -0
  85. package/lib/types/dsh-adapter/oauth/bonus.js +69 -0
  86. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/command.d.ts +2 -1
  87. package/lib/types/dsh-adapter/oauth/command.d.ts.map +1 -0
  88. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/command.js +7 -4
  89. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/credentials.d.ts +13 -10
  90. package/lib/types/dsh-adapter/oauth/credentials.d.ts.map +1 -0
  91. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/credentials.js +61 -23
  92. package/lib/types/dsh-adapter/oauth/deepseek.d.ts +39 -0
  93. package/lib/types/dsh-adapter/oauth/deepseek.d.ts.map +1 -0
  94. package/lib/types/dsh-adapter/oauth/deepseek.js +189 -0
  95. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/index.d.ts +24 -16
  96. package/lib/types/dsh-adapter/oauth/index.d.ts.map +1 -0
  97. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/index.js +34 -20
  98. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/interaction.d.ts +14 -12
  99. package/lib/types/dsh-adapter/oauth/interaction.d.ts.map +1 -0
  100. package/lib/types/dsh-adapter/oauth/interaction.js +283 -0
  101. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/opener.d.ts +2 -1
  102. package/lib/types/dsh-adapter/oauth/opener.d.ts.map +1 -0
  103. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/opener.js +1 -1
  104. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/pi-ai.d.ts +10 -3
  105. package/lib/types/dsh-adapter/oauth/pi-ai.d.ts.map +1 -0
  106. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/pi-ai.js +8 -3
  107. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/profiles.d.ts +6 -3
  108. package/lib/types/dsh-adapter/oauth/profiles.d.ts.map +1 -0
  109. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/profiles.js +11 -3
  110. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/service.d.ts +14 -7
  111. package/lib/types/dsh-adapter/oauth/service.d.ts.map +1 -0
  112. package/{node_modules/@deepseek-harness-tui/dsh-auth/lib → lib/types/dsh-adapter/oauth}/service.js +82 -17
  113. package/lib/types/dsh-adapter/plugin.d.ts +8 -1
  114. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  115. package/lib/types/dsh-adapter/plugin.js +72 -12
  116. package/lib/types/dsh-adapter/presets.d.ts +6 -4
  117. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  118. package/lib/types/dsh-adapter/presets.js +6 -4
  119. package/lib/types/dsh-adapter/providerWizard.d.ts.map +1 -1
  120. package/lib/types/dsh-adapter/providerWizard.js +6 -2
  121. package/lib/types/dsh-adapter/subagents.d.ts +1 -0
  122. package/lib/types/dsh-adapter/subagents.d.ts.map +1 -1
  123. package/lib/types/dsh-adapter/subagents.js +10 -1
  124. package/lib/types/i18n.d.ts +174 -18
  125. package/lib/types/i18n.d.ts.map +1 -1
  126. package/lib/types/i18n.js +53 -11
  127. package/lib/types/ink/components/App.d.ts.map +1 -1
  128. package/lib/types/ink/components/App.js +10 -1
  129. package/lib/types/ink/parse-keypress.d.ts +35 -0
  130. package/lib/types/ink/parse-keypress.d.ts.map +1 -1
  131. package/lib/types/ink/parse-keypress.js +611 -83
  132. package/lib/types/ink/terminal-querier.d.ts +25 -0
  133. package/lib/types/ink/terminal-querier.d.ts.map +1 -1
  134. package/lib/types/ink/terminal-querier.js +42 -0
  135. package/lib/types/ink/update-overflow-guard.d.ts +24 -0
  136. package/lib/types/ink/update-overflow-guard.d.ts.map +1 -1
  137. package/lib/types/ink/update-overflow-guard.js +25 -3
  138. package/lib/types/minimalUiMode.d.ts +19 -0
  139. package/lib/types/minimalUiMode.d.ts.map +1 -0
  140. package/lib/types/minimalUiMode.js +23 -0
  141. package/lib/types/oauth.d.ts +5 -10
  142. package/lib/types/oauth.d.ts.map +1 -1
  143. package/lib/types/oauth.js +5 -10
  144. package/lib/types/screens/Chat.d.ts +4 -1
  145. package/lib/types/screens/Chat.d.ts.map +1 -1
  146. package/lib/types/screens/Chat.js +27 -7
  147. package/lib/types/screens/StatusLine.d.ts +4 -0
  148. package/lib/types/screens/StatusLine.d.ts.map +1 -1
  149. package/lib/types/screens/StatusLine.js +12 -12
  150. package/lib/types/settings/definitions.js +4 -4
  151. package/lib/types/tips.d.ts.map +1 -1
  152. package/lib/types/tips.js +10 -4
  153. package/lib/types/update.js +2 -2
  154. package/lib/types/utils/pastedImagePath.d.ts +8 -0
  155. package/lib/types/utils/pastedImagePath.d.ts.map +1 -1
  156. package/lib/types/utils/pastedImagePath.js +29 -5
  157. package/package.json +111 -100
  158. package/presets/liangshen/agent.cordis.yml +4 -1
  159. package/presets/liangshen/preset.yml +1 -1
  160. package/lib/types/minimalMode.d.ts +0 -13
  161. package/lib/types/minimalMode.d.ts.map +0 -1
  162. package/lib/types/minimalMode.js +0 -17
  163. package/node_modules/@deepseek-harness-tui/dsh-auth/LICENSE +0 -21
  164. package/node_modules/@deepseek-harness-tui/dsh-auth/README.md +0 -143
  165. package/node_modules/@deepseek-harness-tui/dsh-auth/cordis.patch.yml +0 -11
  166. package/node_modules/@deepseek-harness-tui/dsh-auth/dsh-plugin.json +0 -44
  167. package/node_modules/@deepseek-harness-tui/dsh-auth/lib/interaction.js +0 -193
  168. package/node_modules/@deepseek-harness-tui/dsh-auth/package.json +0 -69
@@ -8,17 +8,11 @@ const STATUS_SUFFIX_PATTERN = /^\s*\[status:\s/;
8
8
  function isTerminal(status) {
9
9
  return status === 'completed' || status === 'killed' || status === 'failed';
10
10
  }
11
- /**
12
- * Ordered store of the current conversation's background jobs. Fed by
13
- * {@link BackgroundJobStore.replace} with a fresh `list()` after every
14
- * `onJobsChanged` commit, and by {@link BackgroundJobStore.onOutputSeen}
15
- * with the tail of every `job_output` tool result. Emits no React state of
16
- * its own — the channel wires the events into its version bump, exactly
17
- * like the subagent projection.
18
- */
19
11
  export class BackgroundJobStore {
20
12
  events;
21
13
  jobs = new Map();
14
+ /** Kernel-ring read state per job id (own cursor — never the model's). */
15
+ kernelReads = new Map();
22
16
  /** Commands captured from start acks that arrived before the registry
23
17
  * registered the job (the tool/result stream and the registry commit can
24
18
  * race); consumed on registration. */
@@ -52,16 +46,24 @@ export class BackgroundJobStore {
52
46
  ...(command === undefined ? {} : { command }),
53
47
  status: snap.status,
54
48
  ...(snap.detail === undefined ? {} : { detail: snap.detail }),
49
+ ...(snap.progress === undefined ? {} : { progress: snap.progress }),
55
50
  startedAt: snap.startedAt,
56
51
  ...(snap.finishedAt === undefined ? {} : { finishedAt: snap.finishedAt }),
57
52
  outputLines: [],
53
+ ...(snap.output === undefined ? {} : {
54
+ outputTotalBytes: snap.output.total,
55
+ ...(snap.output.spillPaths === undefined || snap.output.spillPaths.length === 0 ? {} : { spillPaths: [...snap.output.spillPaths] }),
56
+ }),
58
57
  });
58
+ if (snap.output !== undefined)
59
+ this.seedKernelRead(snap.id, snap.output.earliest);
59
60
  changed = true;
60
61
  continue;
61
62
  }
62
63
  if (prev.status === snap.status &&
63
64
  prev.detail === snap.detail &&
64
65
  prev.label === snap.label &&
66
+ prev.progress === snap.progress &&
65
67
  prev.finishedAt === snap.finishedAt)
66
68
  continue;
67
69
  const wasLive = !isTerminal(prev.status);
@@ -71,10 +73,20 @@ export class BackgroundJobStore {
71
73
  delete prev.detail;
72
74
  else
73
75
  prev.detail = snap.detail;
76
+ if (snap.progress === undefined)
77
+ delete prev.progress;
78
+ else
79
+ prev.progress = snap.progress;
74
80
  if (snap.finishedAt === undefined)
75
81
  delete prev.finishedAt;
76
82
  else
77
83
  prev.finishedAt = snap.finishedAt;
84
+ if (snap.output !== undefined) {
85
+ prev.outputTotalBytes = snap.output.total;
86
+ if (snap.output.spillPaths !== undefined && snap.output.spillPaths.length > 0)
87
+ prev.spillPaths = [...snap.output.spillPaths];
88
+ this.seedKernelRead(snap.id, snap.output.earliest);
89
+ }
78
90
  changed = true;
79
91
  if (wasLive && isTerminal(snap.status))
80
92
  this.events.onSettled?.(prev);
@@ -132,13 +144,95 @@ export class BackgroundJobStore {
132
144
  const lines = text
133
145
  .split(/\r?\n/)
134
146
  .map(line => line.replace(/\s+$/, ''))
135
- .filter(line => line !== '' && !STATUS_SUFFIX_PATTERN.test(line));
147
+ .filter(line => line !== '' && !STATUS_SUFFIX_PATTERN.test(line))
148
+ .map(text => ({ text }));
136
149
  if (lines.length === 0)
137
150
  return;
138
151
  job.outputLines = [...job.outputLines, ...lines].slice(-JOBS_MAX_OUTPUT_LINES);
139
152
  job.lastOutputAt = at;
140
153
  this.events.onChanged?.();
141
154
  }
155
+ /**
156
+ * Seed the kernel-ring cursor for one job at its retained head. Called from
157
+ * replace() whenever a snapshot carries ring facts — before any output
158
+ * pull — so the first readAt starts at `earliest`, never at 0 (bytes
159
+ * before `earliest` are already evicted).
160
+ */
161
+ seedKernelRead(id, earliest) {
162
+ const read = this.kernelReads.get(id);
163
+ if (read === undefined)
164
+ this.kernelReads.set(id, { cursor: earliest, partial: '' });
165
+ else if (read.cursor < earliest) {
166
+ // Ring evicted past our cursor: resync and mark the discontinuity.
167
+ read.cursor = earliest;
168
+ read.partial = '';
169
+ const job = this.jobs.get(id);
170
+ if (job !== undefined)
171
+ job.outputDropped = true;
172
+ }
173
+ }
174
+ /**
175
+ * Pull one non-consuming increment from the kernel ring after an `output`
176
+ * event. Chunks split into lines; a chunk without a trailing newline parks
177
+ * its tail until the next pull (producers append streaming bytes, so line
178
+ * boundaries routinely straddle chunks). `gapBefore`/`lossy` mark bytes
179
+ * lost before the retained tail — surfaced as the dropped banner.
180
+ */
181
+ onKernelOutput(id, read, at = Date.now()) {
182
+ const job = this.jobs.get(id);
183
+ if (job === undefined)
184
+ return;
185
+ const state = this.kernelReads.get(id) ?? { cursor: 0, partial: '' };
186
+ this.kernelReads.set(id, state);
187
+ const appended = [];
188
+ let gapPending = read.lossy === true;
189
+ let gapSeen = read.lossy === true;
190
+ for (const chunk of read.chunks) {
191
+ if (chunk.gapBefore === true) {
192
+ gapPending = true;
193
+ gapSeen = true;
194
+ }
195
+ let text = state.partial + chunk.text;
196
+ state.partial = '';
197
+ // A \r not followed by \n is a same-line rewrite (progress spinners);
198
+ // keep the tail after the last one so the panel shows the newest form.
199
+ const lines = text.split(/\r?\n/);
200
+ if (!text.endsWith('\n')) {
201
+ state.partial = lines.pop() ?? '';
202
+ }
203
+ else {
204
+ lines.pop();
205
+ }
206
+ for (const raw of lines) {
207
+ const trimmed = raw.replace(/\s+$/, '');
208
+ if (trimmed === '')
209
+ continue;
210
+ appended.push({ text: trimmed, ...(chunk.channel === undefined ? {} : { channel: chunk.channel }), ...(gapPending ? { gapBefore: true } : {}) });
211
+ gapPending = false;
212
+ }
213
+ if (state.partial !== '') {
214
+ // Re-split parked tail the same way so an embedded \r refreshes it.
215
+ const parts = state.partial.split(/\r(?!\n)/);
216
+ state.partial = parts[parts.length - 1];
217
+ }
218
+ }
219
+ state.cursor = read.next;
220
+ if (gapSeen)
221
+ job.outputDropped = true;
222
+ if (appended.length > 0) {
223
+ job.outputLines = [...job.outputLines, ...appended].slice(-JOBS_MAX_OUTPUT_LINES);
224
+ job.lastOutputAt = at;
225
+ this.events.onChanged?.();
226
+ }
227
+ else if (job.outputTotalBytes !== read.next) {
228
+ this.events.onChanged?.();
229
+ }
230
+ job.outputTotalBytes = Math.max(job.outputTotalBytes ?? 0, read.next);
231
+ }
232
+ /** Kernel read state accessor for tests and cursor diagnostics. */
233
+ kernelCursorOf(id) {
234
+ return this.kernelReads.get(id)?.cursor;
235
+ }
142
236
  /** A fresh snapshot in registration order (newest tracked job last). */
143
237
  snapshot() {
144
238
  return [...this.jobs.values()];
@@ -160,6 +254,7 @@ export class BackgroundJobStore {
160
254
  if (this.jobs.size === 0)
161
255
  return;
162
256
  this.jobs.clear();
257
+ this.kernelReads.clear();
163
258
  this.events.onChanged?.();
164
259
  }
165
260
  }
@@ -0,0 +1,27 @@
1
+ import type { DeepSeekAccount, AccountBonusOrderId, AccountUserId } from '@deepseek-ai/dsh-deepseek-account';
2
+ export interface WhaleCouponNotice {
3
+ readonly accountId: AccountUserId;
4
+ readonly orderId: AccountBonusOrderId;
5
+ readonly amount: string;
6
+ readonly currency: 'CNY' | 'USD';
7
+ readonly expiresAt: string;
8
+ }
9
+ type BonusAccount = Pick<DeepSeekAccount, 'getUnnotifiedBonuses' | 'ackBonusNotified'>;
10
+ /** TUI-only presentation of a Platform-confirmed login grant. */
11
+ export declare class WhaleCouponStore {
12
+ private readonly listeners;
13
+ private readonly seen;
14
+ private notice;
15
+ private account;
16
+ private generation;
17
+ readonly subscribe: (listener: () => void) => (() => void);
18
+ readonly getSnapshot: () => WhaleCouponNotice | null;
19
+ refresh(account: BonusAccount): Promise<void>;
20
+ /** Called only after the modal has painted; a read alone never acknowledges. */
21
+ shown(orderId: AccountBonusOrderId): void;
22
+ dismiss(orderId: AccountBonusOrderId): void;
23
+ clear(): void;
24
+ private emit;
25
+ }
26
+ export {};
27
+ //# sourceMappingURL=bonus.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bonus.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/oauth/bonus.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,mCAAmC,CAAA;AAG5G,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAA;IACjC,QAAQ,CAAC,OAAO,EAAE,mBAAmB,CAAA;IACrC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,QAAQ,EAAE,KAAK,GAAG,KAAK,CAAA;IAChC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;CAC3B;AAED,KAAK,YAAY,GAAG,IAAI,CAAC,eAAe,EAAE,sBAAsB,GAAG,kBAAkB,CAAC,CAAA;AAEtF,iEAAiE;AACjE,qBAAa,gBAAgB;IAC3B,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwB;IAClD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAoB;IACzC,OAAO,CAAC,MAAM,CAAiC;IAC/C,OAAO,CAAC,OAAO,CAA0B;IACzC,OAAO,CAAC,UAAU,CAAI;IAEtB,QAAQ,CAAC,SAAS,GAAI,UAAU,MAAM,IAAI,KAAG,CAAC,MAAM,IAAI,CAAC,CAGxD;IAED,QAAQ,CAAC,WAAW,QAAO,iBAAiB,GAAG,IAAI,CAAe;IAE5D,OAAO,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBnD,gFAAgF;IAChF,KAAK,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI;IASzC,OAAO,CAAC,OAAO,EAAE,mBAAmB,GAAG,IAAI;IAS3C,KAAK,IAAI,IAAI;IAQb,OAAO,CAAC,IAAI;CACb"}
@@ -0,0 +1,69 @@
1
+ import { deepSeekClientMetadata } from './deepseek.js';
2
+ /** TUI-only presentation of a Platform-confirmed login grant. */
3
+ export class WhaleCouponStore {
4
+ listeners = new Set();
5
+ seen = new Set();
6
+ notice = null;
7
+ account;
8
+ generation = 0;
9
+ subscribe = (listener) => {
10
+ this.listeners.add(listener);
11
+ return () => { this.listeners.delete(listener); };
12
+ };
13
+ getSnapshot = () => this.notice;
14
+ async refresh(account) {
15
+ const generation = ++this.generation;
16
+ if (this.listeners.size === 0)
17
+ return;
18
+ let batch;
19
+ try {
20
+ batch = await account.getUnnotifiedBonuses(deepSeekClientMetadata());
21
+ }
22
+ catch {
23
+ return;
24
+ }
25
+ if (generation !== this.generation || batch === null)
26
+ return;
27
+ const granted = batch.bonuses.find(bonus => bonus.campaign === 'dsh_login_bonus'
28
+ && !this.seen.has(`${batch.accountId}:${bonus.orderId}`)
29
+ && (Number.isNaN(Date.parse(bonus.expiresAt)) || Date.parse(bonus.expiresAt) > Date.now()));
30
+ if (granted === undefined)
31
+ return;
32
+ this.account = account;
33
+ this.notice = {
34
+ accountId: batch.accountId, orderId: granted.orderId,
35
+ amount: granted.amount, currency: granted.currency, expiresAt: granted.expiresAt,
36
+ };
37
+ this.emit();
38
+ }
39
+ /** Called only after the modal has painted; a read alone never acknowledges. */
40
+ shown(orderId) {
41
+ const notice = this.notice;
42
+ if (notice?.orderId !== orderId || this.account === undefined)
43
+ return;
44
+ const key = `${notice.accountId}:${notice.orderId}`;
45
+ if (this.seen.has(key))
46
+ return;
47
+ this.seen.add(key);
48
+ void this.account.ackBonusNotified(notice.accountId, orderId, deepSeekClientMetadata()).catch(() => false);
49
+ }
50
+ dismiss(orderId) {
51
+ if (this.notice?.orderId !== orderId)
52
+ return;
53
+ this.shown(orderId);
54
+ const account = this.account;
55
+ this.notice = null;
56
+ this.emit();
57
+ if (account !== undefined)
58
+ void this.refresh(account);
59
+ }
60
+ clear() {
61
+ this.generation++;
62
+ this.notice = null;
63
+ this.account = undefined;
64
+ this.seen.clear();
65
+ this.emit();
66
+ }
67
+ emit() { for (const listener of this.listeners)
68
+ listener(); }
69
+ }
@@ -2,7 +2,7 @@
2
2
  * The `/auth` command: a textual veneer over the {@link DshAuthApi} — status
3
3
  * listing, login (optionally picking the provider interactively), logout.
4
4
  *
5
- * @module dsh-auth/command
5
+ * @module @deepseek-harness-tui/dsh-tui/oauth/command
6
6
  */
7
7
  import type { CommandInvocation, CommandResult } from '@deepseek-ai/dsh-commands';
8
8
  import type { DshAuthApi } from './service.js';
@@ -12,3 +12,4 @@ import type { DshAuthApi } from './service.js';
12
12
  * propagate for the host's own error surface.
13
13
  */
14
14
  export declare function createAuthCommandHandler(api: DshAuthApi): (invocation: CommandInvocation) => Promise<CommandResult>;
15
+ //# sourceMappingURL=command.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/oauth/command.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAA;AACjF,OAAO,KAAK,EAAE,UAAU,EAAuB,MAAM,cAAc,CAAA;AA0BnE;;;;GAIG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,UAAU,GAAG,CAAC,UAAU,EAAE,iBAAiB,KAAK,OAAO,CAAC,aAAa,CAAC,CAiCnH"}
@@ -2,13 +2,15 @@
2
2
  * The `/auth` command: a textual veneer over the {@link DshAuthApi} — status
3
3
  * listing, login (optionally picking the provider interactively), logout.
4
4
  *
5
- * @module dsh-auth/command
5
+ * @module @deepseek-harness-tui/dsh-tui/oauth/command
6
6
  */
7
7
  const USAGE = 'Usage: /auth [status] | /auth login [provider] | /auth logout <provider>';
8
8
  function renderStatus(rows) {
9
9
  const lines = rows.map(row => {
10
10
  const state = row.signedIn
11
- ? `signed in — token expires ${new Date(row.expiresAt ?? 0).toISOString()}`
11
+ ? row.expiresAt === undefined
12
+ ? 'signed in'
13
+ : `signed in — token expires ${new Date(row.expiresAt).toISOString()}`
12
14
  : row.expired
13
15
  ? 'signed in, token expired — /auth login to refresh'
14
16
  : 'not signed in';
@@ -41,8 +43,9 @@ export function createAuthCommandHandler(api) {
41
43
  const result = await api.login(target, invocation.signal);
42
44
  return {
43
45
  kind: 'success',
44
- text: `Signed in to ${result.oauthLabel} (${result.provider}); token expires `
45
- + `${new Date(result.expiresAt).toISOString()}. Its models are selectable via /model.`,
46
+ text: `Signed in to ${result.oauthLabel} (${result.provider})`
47
+ + `${result.expiresAt === undefined ? '' : `; token expires ${new Date(result.expiresAt).toISOString()}`}. `
48
+ + 'Its models are selectable via /model.',
46
49
  };
47
50
  }
48
51
  catch (error) {
@@ -3,15 +3,15 @@
3
3
  * one JSON document, one credential per provider id.
4
4
  *
5
5
  * Writes are atomic (temp file + rename) with 0700 directory / 0600 file
6
- * permissions best-effort on every platform. All mutations go through
7
- * {@link CredentialFile.modify}, which serializes read-modify-write cycles
8
- * per provider in-process — pi-ai runs its OAuth refresh *inside* `modify`,
9
- * so the exclusion here is what keeps concurrent requests from
10
- * double-refreshing a rotated token. The file is the single source of truth;
6
+ * permissions best-effort on every platform. Mutations share an in-process
7
+ * queue and a cross-process file lock; pi-ai runs its OAuth refresh *inside*
8
+ * {@link CredentialFile.modify}, so the exclusion also keeps concurrent
9
+ * requests from double-refreshing a rotated token. The file is the single
10
+ * source of truth;
11
11
  * nothing here ever logs token material, and {@link CredentialFile.describe}
12
12
  * reports only non-secret metadata for status surfaces.
13
13
  *
14
- * @module dsh-auth/credentials
14
+ * @module @deepseek-harness-tui/dsh-tui/oauth/credentials
15
15
  */
16
16
  import type { PiAiCredential, PiAiCredentialInfo, PiAiCredentialStore } from './pi-ai.js';
17
17
  /** The stored credential shape: a pi-ai `OAuthCredential`. */
@@ -30,10 +30,12 @@ export declare function defaultCredentialsFile(): string;
30
30
  */
31
31
  export declare class CredentialFile implements PiAiCredentialStore {
32
32
  readonly path: string;
33
- /** Per-provider operation chains: modify/delete never overlap for one id. */
34
- private readonly chains;
35
- private cache;
33
+ /** One document needs one read-modify-write queue, regardless of provider. */
34
+ private pending;
35
+ private deviceId;
36
36
  constructor(path: string);
37
+ /** Stable per-store UUID for pi-ai's OpenAI ChatGPT agent-host identity. */
38
+ getOrCreateDeviceId(): string;
37
39
  /** The stored credential for one provider, possibly expired. */
38
40
  read(providerId: string): Promise<PiAiCredential | undefined>;
39
41
  /** Stored credential metadata without resolving or exposing secrets. */
@@ -53,8 +55,9 @@ export declare class CredentialFile implements PiAiCredentialStore {
53
55
  expiresAt: number;
54
56
  expired: boolean;
55
57
  }[]>;
56
- /** Run one operation after every earlier operation for the same provider. */
58
+ /** Keep the entire read-modify-rename cycle exclusive across processes. */
57
59
  private chain;
58
60
  private load;
59
61
  private save;
60
62
  }
63
+ //# sourceMappingURL=credentials.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"credentials.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/oauth/credentials.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAOH,OAAO,KAAK,EAAE,cAAc,EAAE,kBAAkB,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAA;AAEzF,8DAA8D;AAC9D,MAAM,MAAM,qBAAqB,GAAG,OAAO,CAAC,cAAc,EAAE;IAAE,IAAI,EAAE,OAAO,CAAA;CAAE,CAAC,CAAA;AAQ9E,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,GAAG,SAAS,CAOpF;AAED,+FAA+F;AAC/F,wBAAgB,sBAAsB,IAAI,MAAM,CAK/C;AAKD;;;;;GAKG;AACH,qBAAa,cAAe,YAAW,mBAAmB;IACxD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAA;IACrB,8EAA8E;IAC9E,OAAO,CAAC,OAAO,CAAmC;IAClD,OAAO,CAAC,QAAQ,CAAoB;gBAExB,IAAI,EAAE,MAAM;IAIxB,4EAA4E;IAC5E,mBAAmB,IAAI,MAAM;IAiC7B,gEAAgE;IAC1D,IAAI,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAInE,wEAAwE;IAClE,IAAI,IAAI,OAAO,CAAC,SAAS,kBAAkB,EAAE,CAAC;IAQpD;;;;;OAKG;IACG,MAAM,CACV,UAAU,EAAE,MAAM,EAClB,EAAE,EAAE,CAAC,OAAO,EAAE,cAAc,GAAG,SAAS,KAAK,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC,GAC/E,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IActC,iDAAiD;IAC3C,MAAM,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAU/C,4EAA4E;IACtE,QAAQ,IAAI,OAAO,CAAC,SAAS;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,OAAO,CAAA;KAAE,EAAE,CAAC;IAY/F,2EAA2E;IAC3E,OAAO,CAAC,KAAK;YAUC,IAAI;YAuCJ,IAAI;CAWnB"}
@@ -3,19 +3,21 @@
3
3
  * one JSON document, one credential per provider id.
4
4
  *
5
5
  * Writes are atomic (temp file + rename) with 0700 directory / 0600 file
6
- * permissions best-effort on every platform. All mutations go through
7
- * {@link CredentialFile.modify}, which serializes read-modify-write cycles
8
- * per provider in-process — pi-ai runs its OAuth refresh *inside* `modify`,
9
- * so the exclusion here is what keeps concurrent requests from
10
- * double-refreshing a rotated token. The file is the single source of truth;
6
+ * permissions best-effort on every platform. Mutations share an in-process
7
+ * queue and a cross-process file lock; pi-ai runs its OAuth refresh *inside*
8
+ * {@link CredentialFile.modify}, so the exclusion also keeps concurrent
9
+ * requests from double-refreshing a rotated token. The file is the single
10
+ * source of truth;
11
11
  * nothing here ever logs token material, and {@link CredentialFile.describe}
12
12
  * reports only non-secret metadata for status surfaces.
13
13
  *
14
- * @module dsh-auth/credentials
14
+ * @module @deepseek-harness-tui/dsh-tui/oauth/credentials
15
15
  */
16
+ import { randomUUID } from 'node:crypto';
16
17
  import { mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
17
18
  import { dirname, join } from 'node:path';
18
19
  import { homedir } from 'node:os';
20
+ import { withFileLock } from '@deepseek-ai/dsh-atomic-write';
19
21
  /** Narrow an unknown parsed value into a stored credential, or reject it. */
20
22
  export function asStoredCredential(value) {
21
23
  if (typeof value !== 'object' || value === null)
@@ -38,6 +40,7 @@ export function defaultCredentialsFile() {
38
40
  return join(root, 'dsh-auth', 'credentials.json');
39
41
  }
40
42
  const EMPTY_DOCUMENT = { version: 1, providers: {} };
43
+ const DEVICE_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/iu;
41
44
  /**
42
45
  * The credential file. IO failures throw (loud, naming the path) rather than
43
46
  * degrading to an empty store: silently treating a corrupt or unreadable
@@ -46,12 +49,49 @@ const EMPTY_DOCUMENT = { version: 1, providers: {} };
46
49
  */
47
50
  export class CredentialFile {
48
51
  path;
49
- /** Per-provider operation chains: modify/delete never overlap for one id. */
50
- chains = new Map();
51
- cache;
52
+ /** One document needs one read-modify-write queue, regardless of provider. */
53
+ pending = Promise.resolve();
54
+ deviceId;
52
55
  constructor(path) {
53
56
  this.path = path;
54
57
  }
58
+ /** Stable per-store UUID for pi-ai's OpenAI ChatGPT agent-host identity. */
59
+ getOrCreateDeviceId() {
60
+ if (this.deviceId !== undefined)
61
+ return this.deviceId;
62
+ const path = join(dirname(this.path), 'device-id');
63
+ let value;
64
+ try {
65
+ value = readFileSync(path, 'utf8');
66
+ }
67
+ catch (error) {
68
+ if (error?.code !== 'ENOENT') {
69
+ throw new Error(`dsh-auth: cannot read device ID file ${path}: ${String(error)}`);
70
+ }
71
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
72
+ try {
73
+ writeFileSync(path, `${randomUUID()}\n`, { flag: 'wx', mode: 0o600 });
74
+ }
75
+ catch (writeError) {
76
+ // A second process may have created the same installation ID first.
77
+ if (writeError?.code !== 'EEXIST') {
78
+ throw new Error(`dsh-auth: cannot write device ID file ${path}: ${String(writeError)}`);
79
+ }
80
+ }
81
+ try {
82
+ value = readFileSync(path, 'utf8');
83
+ }
84
+ catch (readError) {
85
+ throw new Error(`dsh-auth: cannot read device ID file ${path}: ${String(readError)}`);
86
+ }
87
+ }
88
+ const id = value.trim();
89
+ if (!DEVICE_ID_PATTERN.test(id)) {
90
+ throw new Error(`dsh-auth: device ID file ${path} is not a UUID; fix or remove it by hand`);
91
+ }
92
+ this.deviceId = id;
93
+ return id;
94
+ }
55
95
  /** The stored credential for one provider, possibly expired. */
56
96
  async read(providerId) {
57
97
  return (await this.load()).providers[providerId];
@@ -71,7 +111,7 @@ export class CredentialFile {
71
111
  * credential. Rejections from `fn` propagate without touching the file.
72
112
  */
73
113
  async modify(providerId, fn) {
74
- return this.chain(providerId, async () => {
114
+ return this.chain(async () => {
75
115
  const document = await this.load();
76
116
  const current = document.providers[providerId];
77
117
  const replacement = await fn(current);
@@ -86,7 +126,7 @@ export class CredentialFile {
86
126
  }
87
127
  /** Remove one provider's credential (logout). */
88
128
  async delete(providerId) {
89
- await this.chain(providerId, async () => {
129
+ await this.chain(async () => {
90
130
  const document = await this.load();
91
131
  if (!(providerId in document.providers))
92
132
  return;
@@ -107,23 +147,24 @@ export class CredentialFile {
107
147
  expired: credential.expires <= now,
108
148
  }));
109
149
  }
110
- /** Run one operation after every earlier operation for the same provider. */
111
- chain(provider, operation) {
112
- const run = (this.chains.get(provider) ?? Promise.resolve()).then(operation, operation);
113
- this.chains.set(provider, run.then(() => undefined, () => undefined));
150
+ /** Keep the entire read-modify-rename cycle exclusive across processes. */
151
+ chain(operation) {
152
+ const run = this.pending.then(() => {
153
+ mkdirSync(dirname(this.path), { recursive: true, mode: 0o700 });
154
+ // pi-ai may refresh a token over the network while holding this lock.
155
+ return withFileLock(this.path, operation, { waitMs: 120_000 });
156
+ });
157
+ this.pending = run.then(() => undefined, () => undefined);
114
158
  return run;
115
159
  }
116
160
  async load() {
117
- if (this.cache !== undefined)
118
- return this.cache;
119
161
  let text;
120
162
  try {
121
163
  text = readFileSync(this.path, 'utf8');
122
164
  }
123
165
  catch (error) {
124
166
  if (error?.code === 'ENOENT') {
125
- this.cache = EMPTY_DOCUMENT;
126
- return this.cache;
167
+ return EMPTY_DOCUMENT;
127
168
  }
128
169
  throw new Error(`dsh-auth: cannot read credential file ${this.path}: ${String(error)}`);
129
170
  }
@@ -150,13 +191,11 @@ export class CredentialFile {
150
191
  }
151
192
  providers[provider] = credential;
152
193
  }
153
- this.cache = { version: 1, providers };
154
- return this.cache;
194
+ return { version: 1, providers };
155
195
  }
156
196
  async save(document) {
157
197
  const text = JSON.stringify(document, null, 2) + '\n';
158
198
  const directory = dirname(this.path);
159
- mkdirSync(directory, { recursive: true, mode: 0o700 });
160
199
  const temporary = join(directory, `.${Math.random().toString(36).slice(2)}.tmp`);
161
200
  try {
162
201
  writeFileSync(temporary, text, { mode: 0o600 });
@@ -165,6 +204,5 @@ export class CredentialFile {
165
204
  catch (error) {
166
205
  throw new Error(`dsh-auth: cannot write credential file ${this.path}: ${String(error)}`);
167
206
  }
168
- this.cache = document;
169
207
  }
170
208
  }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * TUI interaction for the Host-owned DeepSeek account authorization flow.
3
+ *
4
+ * The Host owns PKCE, its callback route, credential persistence, and the
5
+ * `deepseek-account` LLM route. This module supplies a loopback callback
6
+ * origin, mounts the official Host listener when an older bundle patch omits
7
+ * it, and displays the Host's safe attempt state through the existing question
8
+ * panel. It never reads or writes the account grant.
9
+ */
10
+ import type { Context } from '@deepseek-ai/cordis';
11
+ import type { AccountClientMetadata, DeepSeekAccount } from '@deepseek-ai/dsh-deepseek-account';
12
+ import { type AskFn, type QuestionBridgeHelpers } from './interaction.js';
13
+ export declare const DEEPSEEK_ACCOUNT_PROVIDER = "deepseek-account";
14
+ /** Only the account operations this interaction surface consumes. */
15
+ export type DeepSeekAccountAuth = Pick<DeepSeekAccount, 'getState' | 'startSignIn' | 'cancelSignIn' | 'signOut' | 'watch'> & Partial<Pick<DeepSeekAccount, 'getUnnotifiedBonuses' | 'ackBonusNotified'>>;
16
+ /** Resolve late: the account provider is absent on older DSH hosts. */
17
+ export declare function deepSeekAccountFrom(ctx: Context): DeepSeekAccountAuth | undefined;
18
+ /** The Host webServer may be shared, supplied by the TUI row, or mounted as a fallback. */
19
+ export declare function deepSeekCallbackOrigin(ctx: Context): string;
20
+ /**
21
+ * Keep DeepSeek login usable after a profile-only update from a global TUI
22
+ * whose older bundle patch still mounts `dsh-tui-auth` but has no webserver
23
+ * row. The official Host listener is mounted only on the first login that
24
+ * needs it; an existing Web or user-owned listener is never replaced.
25
+ */
26
+ export declare function createDeepSeekCallbackOriginResolver(ctx: Context): {
27
+ resolve(): Promise<string>;
28
+ dispose(): Promise<void>;
29
+ };
30
+ /** Platform request identity is supplied by the current TUI, not cached by the Host. */
31
+ export declare function deepSeekClientMetadata(): AccountClientMetadata;
32
+ /**
33
+ * Start (or join) the upstream browser attempt, display its URL, and wait for
34
+ * the upstream state stream. Esc/external abort cancels the exact attempt;
35
+ * a commit that won that race is reported as success instead of a false
36
+ * cancellation. The question panel is always retired before returning.
37
+ */
38
+ export declare function loginDeepSeekAccount(account: DeepSeekAccountAuth, callbackOrigin: string, ask: AskFn, signal?: AbortSignal, helpers?: QuestionBridgeHelpers): Promise<void>;
39
+ //# sourceMappingURL=deepseek.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"deepseek.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/oauth/deepseek.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,KAAK,EAAE,qBAAqB,EAAe,eAAe,EAAmB,MAAM,mCAAmC,CAAA;AAG7H,OAAO,EAAkB,KAAK,KAAK,EAAE,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAEzF,eAAO,MAAM,yBAAyB,qBAAqB,CAAA;AAE3D,qEAAqE;AACrE,MAAM,MAAM,mBAAmB,GAAG,IAAI,CAAC,eAAe,EAAE,UAAU,GAAG,aAAa,GAAG,cAAc,GAAG,SAAS,GAAG,OAAO,CAAC,GACtH,OAAO,CAAC,IAAI,CAAC,eAAe,EAAE,sBAAsB,GAAG,kBAAkB,CAAC,CAAC,CAAA;AAE/E,uEAAuE;AACvE,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,OAAO,GAAG,mBAAmB,GAAG,SAAS,CAEjF;AAED,2FAA2F;AAC3F,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAO3D;AAED;;;;;GAKG;AACH,wBAAgB,oCAAoC,CAAC,GAAG,EAAE,OAAO,GAAG;IAClE,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAA;IAC1B,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAA;CACzB,CA0DA;AAWD,wFAAwF;AACxF,wBAAgB,sBAAsB,IAAI,qBAAqB,CAM9D;AAMD;;;;;GAKG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,mBAAmB,EAC5B,cAAc,EAAE,MAAM,EACtB,GAAG,EAAE,KAAK,EACV,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,GAAE,qBAA0B,GAClC,OAAO,CAAC,IAAI,CAAC,CAyDf"}