@deepseek-harness-tui/dsh-tui 0.7.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/README.md +9 -6
  2. package/bin/dsh-tui.js +67 -14
  3. package/cordis.patch.yml +10 -0
  4. package/lib/types/commands.js +1 -1
  5. package/lib/types/components/LoadedContextPanel.d.ts.map +1 -1
  6. package/lib/types/components/LoadedContextPanel.js +1 -1
  7. package/lib/types/components/design-system/ThemedText.d.ts +2 -2
  8. package/lib/types/components/design-system/ThemedText.d.ts.map +1 -1
  9. package/lib/types/components/design-system/ThemedText.js +1 -3
  10. package/lib/types/components/sessions/SessionListRow.d.ts +26 -0
  11. package/lib/types/components/sessions/SessionListRow.d.ts.map +1 -0
  12. package/lib/types/components/sessions/SessionListRow.js +35 -0
  13. package/lib/types/components/sessions/SessionPreview.d.ts +25 -0
  14. package/lib/types/components/sessions/SessionPreview.d.ts.map +1 -0
  15. package/lib/types/components/sessions/SessionPreview.js +60 -0
  16. package/lib/types/dsh-adapter/channel.d.ts +23 -4
  17. package/lib/types/dsh-adapter/channel.d.ts.map +1 -1
  18. package/lib/types/dsh-adapter/channel.js +190 -75
  19. package/lib/types/dsh-adapter/compat/index.d.ts +1 -1
  20. package/lib/types/dsh-adapter/compat/index.d.ts.map +1 -1
  21. package/lib/types/dsh-adapter/compat/index.js +1 -1
  22. package/lib/types/dsh-adapter/compat/sessionLog.d.ts +8 -0
  23. package/lib/types/dsh-adapter/compat/sessionLog.d.ts.map +1 -1
  24. package/lib/types/dsh-adapter/compat/sessionLog.js +1 -1
  25. package/lib/types/dsh-adapter/index.d.ts.map +1 -1
  26. package/lib/types/dsh-adapter/index.js +10 -1
  27. package/lib/types/dsh-adapter/packaged-presets.d.ts +22 -0
  28. package/lib/types/dsh-adapter/packaged-presets.d.ts.map +1 -0
  29. package/lib/types/dsh-adapter/packaged-presets.js +90 -0
  30. package/lib/types/dsh-adapter/plugin.d.ts.map +1 -1
  31. package/lib/types/dsh-adapter/plugin.js +60 -4
  32. package/lib/types/dsh-adapter/presets.d.ts +16 -0
  33. package/lib/types/dsh-adapter/presets.d.ts.map +1 -1
  34. package/lib/types/dsh-adapter/presets.js +23 -0
  35. package/lib/types/dsh-adapter/sessions/digest.d.ts +29 -0
  36. package/lib/types/dsh-adapter/sessions/digest.d.ts.map +1 -0
  37. package/lib/types/dsh-adapter/sessions/digest.js +224 -0
  38. package/lib/types/dsh-adapter/sessions/frames.d.ts +100 -0
  39. package/lib/types/dsh-adapter/sessions/frames.d.ts.map +1 -0
  40. package/lib/types/dsh-adapter/sessions/frames.js +246 -0
  41. package/lib/types/dsh-adapter/sessions/header.d.ts +50 -0
  42. package/lib/types/dsh-adapter/sessions/header.d.ts.map +1 -0
  43. package/lib/types/dsh-adapter/sessions/header.js +62 -0
  44. package/lib/types/dsh-adapter/sessions/index.d.ts +17 -0
  45. package/lib/types/dsh-adapter/sessions/index.d.ts.map +1 -0
  46. package/lib/types/dsh-adapter/sessions/index.js +15 -0
  47. package/lib/types/dsh-adapter/sessions/list.d.ts +41 -0
  48. package/lib/types/dsh-adapter/sessions/list.d.ts.map +1 -0
  49. package/lib/types/dsh-adapter/sessions/list.js +213 -0
  50. package/lib/types/dsh-adapter/sessions/store.d.ts +48 -0
  51. package/lib/types/dsh-adapter/sessions/store.d.ts.map +1 -0
  52. package/lib/types/dsh-adapter/sessions/store.js +164 -0
  53. package/lib/types/dsh-adapter/sessions/types.d.ts +142 -0
  54. package/lib/types/dsh-adapter/sessions/types.d.ts.map +1 -0
  55. package/lib/types/dsh-adapter/sessions/types.js +14 -0
  56. package/lib/types/i18n.d.ts +122 -14
  57. package/lib/types/i18n.d.ts.map +1 -1
  58. package/lib/types/i18n.js +37 -6
  59. package/lib/types/screens/Chat.d.ts +3 -2
  60. package/lib/types/screens/Chat.d.ts.map +1 -1
  61. package/lib/types/screens/Chat.js +28 -144
  62. package/lib/types/screens/SessionBrowser.d.ts +37 -0
  63. package/lib/types/screens/SessionBrowser.d.ts.map +1 -0
  64. package/lib/types/screens/SessionBrowser.js +437 -0
  65. package/lib/types/sessionHistory.d.ts +0 -8
  66. package/lib/types/sessionHistory.d.ts.map +1 -1
  67. package/lib/types/sessions/format.d.ts +117 -0
  68. package/lib/types/sessions/format.d.ts.map +1 -0
  69. package/lib/types/sessions/format.js +244 -0
  70. package/lib/types/sessions/view.d.ts +125 -0
  71. package/lib/types/sessions/view.d.ts.map +1 -0
  72. package/lib/types/sessions/view.js +222 -0
  73. package/package.json +12 -5
  74. package/presets/liangshen/.dsh-tui-managed.json +5 -0
  75. package/presets/liangshen/agent.cordis.yml +389 -0
  76. package/presets/liangshen/compaction-epoch.mjs +81 -0
  77. package/presets/liangshen/custom-bash.mjs +126 -0
  78. package/presets/liangshen/instruction-hint.mjs +181 -0
  79. package/presets/liangshen/preset.yml +3 -0
  80. package/presets/liangshen/skill-search.mjs +142 -0
  81. package/presets/liangshen/tool-bootstrap.mjs +300 -0
  82. package/lib/invariant.js +0 -28
  83. package/lib/types/components/ResumePicker.d.ts +0 -26
  84. package/lib/types/components/ResumePicker.d.ts.map +0 -1
  85. package/lib/types/components/ResumePicker.js +0 -50
@@ -1,15 +1,16 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { assembleContextFor, installModelSelection } from '@deepseek-ai/dsh-agent';
3
- import { isUserInvocable } from '@deepseek-ai/dsh-skill';
3
+ import { isUserInvocable, renderSkillContent } from '@deepseek-ai/dsh-skill';
4
4
  import { createUserMessage, isTokenDelta, MessageId, ReasoningEffortId, } from '@deepseek-ai/dsh-llm';
5
5
  import { runSideQuestion, wrapSideQuestion } from './sideQuestion.js';
6
6
  import { SessionId } from '@deepseek-ai/dsh-session';
7
7
  import { renderContextSections, renderPrompt } from '@deepseek-ai/dsh-system-prompt';
8
8
  import { loadBaselineInstructions } from '@deepseek-ai/dsh-agent-instructions';
9
9
  import { extname, isAbsolute, join } from 'node:path';
10
- import { completeCommands, LOCAL_COMMANDS } from '../commands.js';
11
- import { clearResumeTarget, forgetSession, readLastUsed, readResumeTarget, touchSession, writeResumeTarget } from '../sessionHistory.js';
12
- import { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, readSessionTitleFromLog, sessionsRoots } from './compat/index.js';
10
+ import { completeCommands, isLocalCommandName, LOCAL_COMMANDS, parseCommandName } from '../commands.js';
11
+ import { clearResumeTarget, forgetSession, readResumeTarget, touchSession, writeResumeTarget } from '../sessionHistory.js';
12
+ import { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, sessionsRoots } from './compat/index.js';
13
+ import { listSummaries, locateSession, noteBranch, previewSession, } from './sessions/index.js';
13
14
  import { writeActivityFrames } from '../activityPrefs.js';
14
15
  import { readEffortPref, writeEffortPref } from '../effortPrefs.js';
15
16
  import { readModelPref, writeModelPref } from '../modelPrefs.js';
@@ -26,6 +27,11 @@ import { modeDisplayName, resolveSessionModes } from '../sessionModes.js';
26
27
  import { ActivityTracker } from 'dsh-working-activity/status';
27
28
  import { attachSessionToWorkspace } from './workspace.js';
28
29
  import { createLocalWorkspaceRuntime } from './workspaces.js';
30
+ /**
31
+ * Delay before re-reading a skill catalog that reported an incomplete
32
+ * observation (a provider whose directory watcher is still warming).
33
+ */
34
+ const SKILL_COMMAND_RETRY_MS = 800;
29
35
  const ARGS_PREVIEW_LIMIT = 160;
30
36
  const RESULT_PREVIEW_LIMIT = 240;
31
37
  /** Local `!`-command output cap (mirrors the result preview limit). */
@@ -251,18 +257,8 @@ function coalesceReplayEvents(events) {
251
257
  }
252
258
  /** Buffer below the context window at which CC warns (autoCompact.ts). */
253
259
  const CONTEXT_WARNING_BUFFER_TOKENS = 20_000;
254
- /** How many newest sessions resolve their title from the first user message
255
- * (persistence.load reads the whole log — depth caps the picker latency). */
256
- const SESSION_TITLE_DEPTH = 20;
257
- /** Picker title cap, in characters. */
258
- const SESSION_TITLE_LIMIT = 40;
259
- /** One-line session title: whitespace folded, capped with an ellipsis. */
260
- function shortenTitle(text) {
261
- const flat = text.replace(/\s+/g, ' ').trim();
262
- return flat.length <= SESSION_TITLE_LIMIT
263
- ? flat
264
- : `${flat.slice(0, SESSION_TITLE_LIMIT - 1)}…`;
265
- }
260
+ /** How many trailing exchanges the browser's preview pane asks for. */
261
+ const PREVIEW_ENTRIES = 8;
266
262
  /** Resolve once a `turn/end` event newer than `fromSeq` lands in the session
267
263
  * log (Agent.cancel closes the turn asynchronously), or when the timeout
268
264
  * expires. Polling the session log is race-free here: fork reads the same
@@ -298,7 +294,11 @@ export function createChannel(ctx, initialAgent, options) {
298
294
  // command/run + command/done records). Absent the service, only the
299
295
  // built-in local commands exist.
300
296
  const commandService = ctx.get('commands');
301
- const workspaceService = ctx.tuiWorkspaces ?? createLocalWorkspaceRuntime();
297
+ // Workspace registry runtime (optional service, issue #183): mounted by
298
+ // the bundle patch's dsh-tui-workspaces row; absent the row (stale patch
299
+ // or a bare embedder), degrade to the local-only runtime. plugin.ts owns
300
+ // the degraded-boot warning for profile launches.
301
+ const workspaceService = ctx.get('tuiWorkspaces') ?? createLocalWorkspaceRuntime();
302
302
  const commandTrees = ctx.get('tuiCommandTrees');
303
303
  // Shift+Tab session-mode cycle: cordis.yml `modes` wins; absent/empty/
304
304
  // atom-less → the built-in default/plan/full cycle (sessionModes.ts).
@@ -977,6 +977,7 @@ export function createChannel(ctx, initialAgent, options) {
977
977
  bindAgent();
978
978
  refreshCommandList();
979
979
  void refreshLoadedContext();
980
+ void refreshSkillCommands();
980
981
  // The forked session (rewind) becomes the most recently used.
981
982
  touchSession(childId);
982
983
  state.emit();
@@ -1103,6 +1104,7 @@ export function createChannel(ctx, initialAgent, options) {
1103
1104
  bindAgent();
1104
1105
  refreshCommandList();
1105
1106
  void refreshLoadedContext();
1107
+ void refreshSkillCommands();
1106
1108
  // Keep the `--resume` launcher contract pointing at the same session.
1107
1109
  writeResumeTarget(sessionId);
1108
1110
  // The resumed session is now the most recently used.
@@ -1225,6 +1227,7 @@ export function createChannel(ctx, initialAgent, options) {
1225
1227
  bindAgent();
1226
1228
  refreshCommandList();
1227
1229
  void refreshLoadedContext();
1230
+ void refreshSkillCommands();
1228
1231
  clearResumeTarget();
1229
1232
  // The brand-new session becomes the most recently used.
1230
1233
  touchSession(handle.agent.id);
@@ -1398,6 +1401,7 @@ export function createChannel(ctx, initialAgent, options) {
1398
1401
  bindAgent();
1399
1402
  refreshCommandList();
1400
1403
  void refreshLoadedContext();
1404
+ void refreshSkillCommands();
1401
1405
  // The model-switched fork becomes the most recently used.
1402
1406
  touchSession(childId);
1403
1407
  state.emit();
@@ -1661,61 +1665,21 @@ export function createChannel(ctx, initialAgent, options) {
1661
1665
  return listFilesDeep(fs, state.cwd);
1662
1666
  },
1663
1667
  async listSessions() {
1664
- // DSH's own session index: the persistence backend materializes one
1665
- // entry per durable session log (headers carry cwd + createdAt).
1668
+ // Every stored session, classified and unfiltered. Which of them a
1669
+ // surface shows — this project only, conversations only, sub-agent runs
1670
+ // folded away — is a view decision, and keeping it out of here is what
1671
+ // lets the browser toggle those views without re-reading a single log.
1666
1672
  const persistence = ctx.get('sessionPersistence');
1667
1673
  if (!persistence)
1668
1674
  return [];
1669
- try {
1670
- const headers = await persistence.list();
1671
- // 按工作目录隔离(Claude Code 的项目维度):/resume 只列出本会话
1672
- // 目录启动的会话,别的项目的会话不出现在选择器里。
1673
- const local = headers.filter(header => sessionCwdMatches(state.cwd, header.cwd ?? ''));
1674
- // MRU ordering: DSH headers carry only createdAt, so dsh-tui keeps its
1675
- // own last-used timestamps (touchSession on resume/submit/new) and
1676
- // falls back to createdAt for sessions never touched in this install.
1677
- const lastUsed = readLastUsed();
1678
- const records = local
1679
- .map(header => ({
1680
- id: header.id,
1681
- // Titles load lazily below (first user message); until then the
1682
- // cwd basename stands in (matching the status line), with a
1683
- // short id when absent.
1684
- title: basename(header.cwd ?? '') || `session ${String(header.id).slice(0, 8)}`,
1685
- cwd: header.cwd ?? '',
1686
- createdAt: header.createdAt,
1687
- updatedAt: lastUsed[header.id] ?? header.createdAt,
1688
- }))
1689
- .sort((a, b) => b.updatedAt - a.updatedAt);
1690
- // Title = the session's session/title event (auto: first prompt;
1691
- // manual: /rename), else its FIRST user message — the picker's most
1692
- // useful label. Read via the tolerant compat log reader instead of
1693
- // persistence.load: the backend validates every event against
1694
- // KNOWN_SESSION_EVENT_TYPES and throws the whole load on an unmarked
1695
- // an unregistered third-party type, which would otherwise leave the
1696
- // affected session titled with the cwd
1697
- // basename. An unreadable log keeps the basename fallback.
1698
- const empty = new Set();
1699
- for (const record of records.slice(0, SESSION_TITLE_DEPTH)) {
1700
- const info = readSessionTitleFromLog(String(record.id));
1701
- if (info === undefined)
1702
- continue; // keep the basename fallback
1703
- if (!info.hasUserMessage) {
1704
- // Launch artifact — a session with no user message holds no
1705
- // conversation to resume, so drop it from the picker (its
1706
- // createdAt-only updatedAt would otherwise pin it near the
1707
- // top forever, one per dsh-tui launch).
1708
- empty.add(record.id);
1709
- continue;
1710
- }
1711
- if (info.title !== undefined)
1712
- record.title = shortenTitle(info.title);
1713
- }
1714
- return records.filter(record => !empty.has(record.id));
1715
- }
1716
- catch {
1675
+ return listSummaries(persistence);
1676
+ },
1677
+ async previewSession(sessionId) {
1678
+ const persistence = ctx.get('sessionPersistence');
1679
+ if (!persistence)
1717
1680
  return [];
1718
- }
1681
+ const path = await locateSession(persistence, sessionId);
1682
+ return path === undefined ? [] : previewSession(path, PREVIEW_ENTRIES);
1719
1683
  },
1720
1684
  setResumeTarget(sessionId) {
1721
1685
  writeResumeTarget(sessionId);
@@ -1755,12 +1719,10 @@ export function createChannel(ctx, initialAgent, options) {
1755
1719
  }
1756
1720
  if (appendSessionTitle(sessionId, title) !== 'appended')
1757
1721
  return false;
1758
- // listSessions resolves persisted titles only for the MRU top
1759
- // SESSION_TITLE_DEPTH; a rename does not change MRU by itself, so a
1760
- // session beyond the window would keep showing the cwd-basename
1761
- // fallback (in the next picker AND after restart) even though the
1762
- // title event is durable. A rename IS user interaction with the
1763
- // session — touching it pulls it into the title window.
1722
+ // The append changed the log, so the next listing sees a new revision,
1723
+ // re-derives, and reads back the very title event just written — no
1724
+ // second path to the same answer. Touching it is about ordering, not
1725
+ // titles: a rename is user interaction, so the row belongs at the top.
1764
1726
  touchSession(sessionId);
1765
1727
  return true;
1766
1728
  },
@@ -1973,6 +1935,9 @@ export function createChannel(ctx, initialAgent, options) {
1973
1935
  return [t('subagent-query-failed', { err: error instanceof Error ? error.message : String(error) })];
1974
1936
  }
1975
1937
  },
1938
+ releaseContributions() {
1939
+ releaseSkillCommands();
1940
+ },
1976
1941
  traceEvents() {
1977
1942
  // Immutable per-append snapshot (dsh-session caches the frozen array);
1978
1943
  // reads follow agent swaps (/resume /rewind /new) automatically.
@@ -2093,6 +2058,11 @@ export function createChannel(ctx, initialAgent, options) {
2093
2058
  ...(descriptions === undefined ? {} : { descriptions }),
2094
2059
  tag: descriptor.input?.hint,
2095
2060
  external: true,
2061
+ // Skills reach the registry as ordinary commands, so the menu would
2062
+ // lose the marker HelpMenu uses to keep them out of the chrome list.
2063
+ // This channel registered them and is the authority on which names
2064
+ // are skills.
2065
+ ...(skillCommands.has(descriptor.name) ? { skill: true } : {}),
2096
2066
  });
2097
2067
  }
2098
2068
  }
@@ -2165,8 +2135,148 @@ export function createChannel(ctx, initialAgent, options) {
2165
2135
  };
2166
2136
  ctx.on('commands/change', refreshCommandList);
2167
2137
  ctx.on('skills/change', refreshCommandList);
2138
+ /**
2139
+ * The view a skill-catalog read must be taken through, as ONE value.
2140
+ *
2141
+ * The registry is host-plane but scope-LAYERED: a provider mounted by an
2142
+ * agent preset's standing composition files into that preset's layer, and a
2143
+ * read taken without the scope sees only the host layer. Passing the pair
2144
+ * together keeps a read from being taken half-scoped.
2145
+ *
2146
+ * @param target - the agent whose view is wanted.
2147
+ */
2148
+ const skillViewOptions = (target) => ({
2149
+ scope: target,
2150
+ cwd: state.cwd,
2151
+ });
2152
+ /** The skill registry as the given agent sees it, or undefined when a boot
2153
+ * mounts none. `serviceForAgent` resolves through the agent's mount and
2154
+ * falls back to the host context. */
2155
+ const skillRegistryFor = (target) => serviceForAgent(ctx, target, 'skills');
2156
+ /**
2157
+ * Skill commands this channel owns, by skill name. The value keeps the
2158
+ * description the command was registered with so an edited SKILL.md
2159
+ * re-registers instead of leaving a stale menu entry.
2160
+ */
2161
+ const skillCommands = new Map();
2162
+ /** Skill names the registry refused (name taken, or invalid) — warn once. */
2163
+ const skillCommandsRefused = new Set();
2164
+ /** Pending re-read after an incomplete catalog observation. */
2165
+ let skillCommandsRetry;
2166
+ /**
2167
+ * Publish every user-invocable skill as a slash command (issue #86).
2168
+ *
2169
+ * The completion menu already lists these skills, but a menu entry is not a
2170
+ * command: nothing dispatches it, so typing the name and pressing Enter does
2171
+ * nothing. Registering through the host command registry is what makes them
2172
+ * runnable, and buys three things the TUI would otherwise reimplement:
2173
+ * `register` emits `commands/change`, so the menu merge folds the entry in
2174
+ * on its own; Enter dispatches through the normal command path, so the
2175
+ * invocation is logged as a paired `command/run`/`command/done` like every
2176
+ * other command; and the handler runs host-side, so invoking a skill is
2177
+ * DETERMINISTIC — the body is injected here, instead of sending `/name` to
2178
+ * the model and depending on it to recognize the text and reach for its
2179
+ * skill loader.
2180
+ *
2181
+ * `userInvocable` covers "human-facing command catalogs AND loaders", so
2182
+ * discovery alone would honor half the flag.
2183
+ */
2184
+ const refreshSkillCommands = async () => {
2185
+ if (commandService === undefined)
2186
+ return;
2187
+ const target = agent;
2188
+ const registry = skillRegistryFor(target);
2189
+ if (registry === undefined)
2190
+ return;
2191
+ let observation;
2192
+ try {
2193
+ observation = await registry.snapshot(skillViewOptions(target));
2194
+ }
2195
+ catch (error) {
2196
+ ctx.logger.warn('skill commands: catalog read failed: %o', error);
2197
+ return;
2198
+ }
2199
+ if (target !== agent)
2200
+ return;
2201
+ // A provider still warming its watcher reports an incomplete observation;
2202
+ // re-read once so a cold start cannot leave the menu permanently short.
2203
+ if (!observation.complete && skillCommandsRetry === undefined) {
2204
+ skillCommandsRetry = setTimeout(() => {
2205
+ skillCommandsRetry = undefined;
2206
+ void refreshSkillCommands();
2207
+ }, SKILL_COMMAND_RETRY_MS);
2208
+ }
2209
+ const wanted = new Map(observation.skills
2210
+ .filter(skill => isUserInvocable(skill))
2211
+ // A name the TUI's own command grammar cannot parse would show in the
2212
+ // menu and then fail to dispatch when typed; ask the real parser
2213
+ // instead of restating its pattern here.
2214
+ .filter(skill => parseCommandName(`/${skill.name}`)?.name === skill.name)
2215
+ // Built-in locals win a name collision, exactly as they do over
2216
+ // plugin-registered commands in refreshCommandList.
2217
+ .filter(skill => !isLocalCommandName(skill.name))
2218
+ .map(skill => [skill.name, skill.description]));
2219
+ for (const [name, entry] of skillCommands) {
2220
+ if (wanted.get(name) === entry.description)
2221
+ continue;
2222
+ entry.dispose();
2223
+ skillCommands.delete(name);
2224
+ }
2225
+ for (const [name, description] of wanted) {
2226
+ if (skillCommands.has(name) || skillCommandsRefused.has(name))
2227
+ continue;
2228
+ // Another plugin already owns this name (plan/goal/…): leave it alone.
2229
+ if (commandService.find(target, name) !== undefined)
2230
+ continue;
2231
+ try {
2232
+ const dispose = commandService.register({
2233
+ name,
2234
+ description,
2235
+ // The injected body is the payload; recording the (empty) raw input
2236
+ // would only duplicate the command name into the session log.
2237
+ recordInput: false,
2238
+ handler: async ({ agent: invoker, signal }) => {
2239
+ const view = { ...skillViewOptions(invoker), signal };
2240
+ const skill = await skillRegistryFor(invoker)?.get(name, view);
2241
+ if (skill === undefined || !isUserInvocable(skill)) {
2242
+ return { kind: 'error', text: t('skill-unavailable', { name }) };
2243
+ }
2244
+ // The official user-explicit invocation shape (dsh-skill's
2245
+ // SkillInvocationSource): the rendered body rides as instructions
2246
+ // the model follows, and transcript consumers present it from the
2247
+ // source metadata instead of re-parsing model-facing text.
2248
+ invoker.followup(createUserMessage({
2249
+ content: [{ type: 'text', text: renderSkillContent(skill) }],
2250
+ source: { kind: 'skill-invocation', name, form: 'instructions' },
2251
+ }));
2252
+ // Silent success: the agent visibly starts working on the skill,
2253
+ // which is the feedback (CC shows no banner either).
2254
+ return { kind: 'success' };
2255
+ },
2256
+ });
2257
+ skillCommands.set(name, { dispose, description });
2258
+ }
2259
+ catch (error) {
2260
+ skillCommandsRefused.add(name);
2261
+ ctx.logger.warn(`skill commands: "${name}" not registrable: %o`, error);
2262
+ }
2263
+ }
2264
+ };
2265
+ ctx.on('skills/change', () => {
2266
+ void refreshSkillCommands();
2267
+ });
2268
+ /** See {@link Channel.releaseContributions}. */
2269
+ const releaseSkillCommands = () => {
2270
+ if (skillCommandsRetry !== undefined)
2271
+ clearTimeout(skillCommandsRetry);
2272
+ skillCommandsRetry = undefined;
2273
+ for (const entry of skillCommands.values())
2274
+ entry.dispose();
2275
+ skillCommands.clear();
2276
+ };
2168
2277
  refreshCommandList();
2169
2278
  void refreshLoadedContext();
2279
+ void refreshSkillCommands();
2170
2280
  let nextRowId = 0;
2171
2281
  /** The leaf's bash executor (dsh-bash-local in the example leaf) — the DSH
2172
2282
  * execution seam for local `!` commands and the git status breadcrumb. The
@@ -2915,6 +3025,11 @@ ${output}
2915
3025
  const branch = result.stdout.text.trim();
2916
3026
  if (branch !== '') {
2917
3027
  state.gitBranch = branch;
3028
+ // Note it against the session too. A session log records no branch,
3029
+ // and nothing can reconstruct one after the fact, so the browser can
3030
+ // only show a branch for sessions this install actually used — which
3031
+ // is exactly what the column claims.
3032
+ noteBranch(agent.session.id, branch);
2918
3033
  state.emit();
2919
3034
  }
2920
3035
  })
@@ -2927,7 +3042,7 @@ ${output}
2927
3042
  refreshGitBranch();
2928
3043
  return state;
2929
3044
  }
2930
- /** Path basename for the resume-list title (`C:/a/b` → `b`). */
3045
+ /** Trailing path segment (`C:/a/b` → `b`). */
2931
3046
  function basename(path) {
2932
3047
  const parts = path.split(/[\\/]/);
2933
3048
  return parts[parts.length - 1] ?? path;
@@ -18,5 +18,5 @@
18
18
  * registration API (issue #153).
19
19
  * @module @deepseek-harness-tui/dsh-tui/compat
20
20
  */
21
- export { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, LEGACY_SESSION_EVENT_TYPES, readSessionTitleFromLog, sessionsRoots, } from './sessionLog.js';
21
+ export { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, findSessionLogFile, LEGACY_SESSION_EVENT_TYPES, readSessionTitleFromLog, sessionsRoots, } from './sessionLog.js';
22
22
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/compat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,6BAA6B,EAC7B,0BAA0B,EAC1B,uBAAuB,EACvB,aAAa,GACd,MAAM,iBAAiB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/compat/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,OAAO,EACL,kBAAkB,EAClB,gBAAgB,EAChB,6BAA6B,EAC7B,kBAAkB,EAClB,0BAA0B,EAC1B,uBAAuB,EACvB,aAAa,GACd,MAAM,iBAAiB,CAAA"}
@@ -18,4 +18,4 @@
18
18
  * registration API (issue #153).
19
19
  * @module @deepseek-harness-tui/dsh-tui/compat
20
20
  */
21
- export { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, LEGACY_SESSION_EVENT_TYPES, readSessionTitleFromLog, sessionsRoots, } from './sessionLog.js';
21
+ export { appendSessionTitle, deleteSessionLog, ensureLegacySessionEventTypes, findSessionLogFile, LEGACY_SESSION_EVENT_TYPES, readSessionTitleFromLog, sessionsRoots, } from './sessionLog.js';
@@ -14,6 +14,14 @@ export declare const LEGACY_SESSION_EVENT_TYPES: readonly string[];
14
14
  * explicit DSH_TUI_SESSION_ROOT always outranks the defaults.
15
15
  */
16
16
  export declare function sessionsRoots(): string[];
17
+ /**
18
+ * Locate a session's log by scanning workspace directories for the session
19
+ * id — deliberately NOT replicating the persistence plugin's workspace-key
20
+ * sanitization, so the helpers survive upstream key-scheme changes.
21
+ * @param sessionId - Session id (directory name under each workspace dir).
22
+ * @returns Absolute path of session.jsonl.zstd, or undefined when absent.
23
+ */
24
+ export declare function findSessionLogFile(sessionId: string): string | undefined;
17
25
  /**
18
26
  * Register every {@link LEGACY_SESSION_EVENT_TYPES} type as known in EVERY
19
27
  * reachable KNOWN_SESSION_EVENT_TYPES copy, ahead of the strict read path
@@ -1 +1 @@
1
- {"version":3,"file":"sessionLog.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/compat/sessionLog.ts"],"names":[],"mappings":"AAqDA;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EAAwB,CAAA;AAKhF;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CASxC;AAkED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAkCpD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAChB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAyBzD;AAyBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,CAwB/F;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CAwB7E"}
1
+ {"version":3,"file":"sessionLog.d.ts","sourceRoot":"","sources":["../../../../src/dsh-adapter/compat/sessionLog.ts"],"names":[],"mappings":"AAqDA;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EAAwB,CAAA;AAKhF;;;;;;;GAOG;AACH,wBAAgB,aAAa,IAAI,MAAM,EAAE,CASxC;AAaD;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAexE;AA+BD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,6BAA6B,IAAI,IAAI,CAkCpD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,MAAM,GAChB;IAAE,KAAK,CAAC,EAAE,MAAM,CAAC;IAAC,cAAc,EAAE,OAAO,CAAA;CAAE,GAAG,SAAS,CAyBzD;AAyBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,UAAU,GAAG,aAAa,CAwB/F;AAED;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,GAAG,aAAa,CAwB7E"}
@@ -88,7 +88,7 @@ function isSafeSessionId(sessionId) {
88
88
  * @param sessionId - Session id (directory name under each workspace dir).
89
89
  * @returns Absolute path of session.jsonl.zstd, or undefined when absent.
90
90
  */
91
- function findSessionLogFile(sessionId) {
91
+ export function findSessionLogFile(sessionId) {
92
92
  if (!isSafeSessionId(sessionId))
93
93
  return undefined;
94
94
  for (const root of sessionsRoots()) {
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,eAAO,MAAM,IAAI,YAAY,CAAA;AAC7B,eAAO,MAAM,MAAM,UAA8B,CAAA;AAEjD;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;yEAKqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;gDAC4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;gFAE4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;oCACgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;sBAIkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;4BAGwB;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;wDAEoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;kFAC8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;gCAG4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;yDAGqD;IACrD,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;CAC1B;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CA0BhC,CAAA;AAEF;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWvE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,MAAM,MAAM,0BAA0B,CAAA;AAC7C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEzD,eAAO,MAAM,IAAI,YAAY,CAAA;AAU7B,eAAO,MAAM,MAAM,UAAa,CAAA;AAEhC;;;GAGG;AACH,MAAM,WAAW,MAAM;IACrB,0EAA0E;IAC1E,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;yEAKqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB;gDAC4C;IAC5C,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;gFAE4E;IAC5E,GAAG,CAAC,EAAE,MAAM,CAAA;IACZ;oCACgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;sBAIkB;IAClB,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,OAAO,CAAA;IAClB;;;4BAGwB;IACxB,cAAc,CAAC,EAAE,MAAM,CAAA;IACvB;;wDAEoD;IACpD,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,OAAO,CAAA;IACpB;kFAC8E;IAC9E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb;;;gCAG4B;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;yDAGqD;IACrD,KAAK,CAAC,EAAE,eAAe,EAAE,CAAA;CAC1B;AAED,eAAO,MAAM,MAAM,EAAE,MAAM,CAAC,MAAM,CA0BhC,CAAA;AAEF;;;;;;GAMG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAWvE"}
@@ -1,6 +1,15 @@
1
1
  import Schema from '@deepseek-ai/schemastery';
2
2
  export const name = 'dsh-tui';
3
- export const inject = ['agents', 'tuiWorkspaces'];
3
+ // `tuiWorkspaces` must stay OUT of this code-level inject (issue #183): the
4
+ // dsh CLI resolves the bundle's cordis.patch.yml from the FIRST copy of this
5
+ // package found from its own install anchor (typically the global launcher),
6
+ // while the Loader imports the plugin module from the profile's copy. When
7
+ // the two copies skew, the patch may predate the dsh-tui-workspaces row — a
8
+ // hard inject here then deadlocks the whole tree at boot ("pending (waiting
9
+ // for service: tuiWorkspaces)"). The bundle patch keeps tuiWorkspaces in the
10
+ // row-level inject purely as an ordering guarantee when the row exists; when
11
+ // it does not, plugin.ts/channel.ts fall back to a local workspace runtime.
12
+ export const inject = ['agents'];
4
13
  export const Config = Schema.object({
5
14
  sessionId: Schema.string().required(false),
6
15
  // No schema defaults on the route: a `.default()` here would make an
@@ -0,0 +1,22 @@
1
+ export interface PackagedPresetResult {
2
+ id: string;
3
+ status: 'installed' | 'updated' | 'current' | 'conflict';
4
+ }
5
+ export interface PackagedPresetOptions {
6
+ dshHome?: string;
7
+ sourceRoot?: string;
8
+ moduleUrl?: string;
9
+ }
10
+ /** Resolve the package asset in both src/tsx and compiled npm layouts. */
11
+ export declare function packagedPresetRoot(moduleUrl?: string): string;
12
+ /**
13
+ * Materialize presets shipped by dsh-tui into the Harness user preset root.
14
+ *
15
+ * The official launcher replaces the roster's configured roots with its own
16
+ * shipped root at the end of profile composition, so a bundle patch cannot
17
+ * add a second system root. The user root is the roster's supported extension
18
+ * seam and is discovered on every list/resolve call. Existing unmarked
19
+ * directories are never overwritten.
20
+ */
21
+ export declare function ensurePackagedPresets(options?: PackagedPresetOptions): PackagedPresetResult[];
22
+ //# sourceMappingURL=packaged-presets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packaged-presets.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/packaged-presets.ts"],"names":[],"mappings":"AAuBA,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,WAAW,GAAG,SAAS,GAAG,SAAS,GAAG,UAAU,CAAA;CACzD;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB;AAmBD,0EAA0E;AAC1E,wBAAgB,kBAAkB,CAAC,SAAS,GAAE,MAAwB,GAAG,MAAM,CAQ9E;AAED;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,GAAE,qBAA0B,GAAG,oBAAoB,EAAE,CAkDjG"}
@@ -0,0 +1,90 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, } from 'node:fs';
3
+ import { homedir } from 'node:os';
4
+ import { dirname, join } from 'node:path';
5
+ import { fileURLToPath } from 'node:url';
6
+ const OWNER = '@deepseek-harness-tui/dsh-tui';
7
+ const MARKER = '.dsh-tui-managed.json';
8
+ function readMarker(directory) {
9
+ try {
10
+ const value = JSON.parse(readFileSync(join(directory, MARKER), 'utf8'));
11
+ if (value.owner === OWNER
12
+ && typeof value.preset === 'string'
13
+ && typeof value.revision === 'string'
14
+ && value.revision.length > 0) {
15
+ return value;
16
+ }
17
+ }
18
+ catch {
19
+ // An absent or user-authored marker means this directory is not ours.
20
+ }
21
+ return undefined;
22
+ }
23
+ /** Resolve the package asset in both src/tsx and compiled npm layouts. */
24
+ export function packagedPresetRoot(moduleUrl = import.meta.url) {
25
+ const directory = dirname(fileURLToPath(moduleUrl));
26
+ const candidates = [join(directory, '../../presets'), join(directory, '../../../presets')];
27
+ const found = candidates.find(candidate => existsSync(candidate));
28
+ if (found === undefined) {
29
+ throw new Error(`dsh-tui: packaged preset root is missing (checked ${candidates.join(', ')})`);
30
+ }
31
+ return found;
32
+ }
33
+ /**
34
+ * Materialize presets shipped by dsh-tui into the Harness user preset root.
35
+ *
36
+ * The official launcher replaces the roster's configured roots with its own
37
+ * shipped root at the end of profile composition, so a bundle patch cannot
38
+ * add a second system root. The user root is the roster's supported extension
39
+ * seam and is discovered on every list/resolve call. Existing unmarked
40
+ * directories are never overwritten.
41
+ */
42
+ export function ensurePackagedPresets(options = {}) {
43
+ const sourceRoot = options.sourceRoot ?? packagedPresetRoot(options.moduleUrl);
44
+ const dshHome = options.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh');
45
+ const targetRoot = join(dshHome, '.agent-presets');
46
+ const results = [];
47
+ for (const entry of readdirSync(sourceRoot, { withFileTypes: true })) {
48
+ if (!entry.isDirectory())
49
+ continue;
50
+ const id = entry.name;
51
+ const source = join(sourceRoot, id);
52
+ const sourceMarker = readMarker(source);
53
+ if (sourceMarker === undefined || sourceMarker.preset !== id) {
54
+ throw new Error(`dsh-tui: packaged preset ${id} has no valid ${MARKER}`);
55
+ }
56
+ const target = join(targetRoot, id);
57
+ if (!existsSync(target)) {
58
+ mkdirSync(targetRoot, { recursive: true });
59
+ cpSync(source, target, { recursive: true, force: false, errorOnExist: true });
60
+ results.push({ id, status: 'installed' });
61
+ continue;
62
+ }
63
+ const targetMarker = readMarker(target);
64
+ if (targetMarker === undefined || targetMarker.preset !== id) {
65
+ results.push({ id, status: 'conflict' });
66
+ continue;
67
+ }
68
+ if (targetMarker.revision === sourceMarker.revision) {
69
+ results.push({ id, status: 'current' });
70
+ continue;
71
+ }
72
+ const suffix = `${process.pid}-${randomUUID()}`;
73
+ const staged = join(targetRoot, `.${id}.staged-${suffix}`);
74
+ const backup = join(targetRoot, `.${id}.backup-${suffix}`);
75
+ cpSync(source, staged, { recursive: true, force: false, errorOnExist: true });
76
+ try {
77
+ renameSync(target, backup);
78
+ renameSync(staged, target);
79
+ rmSync(backup, { recursive: true, force: true });
80
+ }
81
+ catch (error) {
82
+ if (!existsSync(target) && existsSync(backup))
83
+ renameSync(backup, target);
84
+ rmSync(staged, { recursive: true, force: true });
85
+ throw error;
86
+ }
87
+ results.push({ id, status: 'updated' });
88
+ }
89
+ return results;
90
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAe,MAAM,wBAAwB,CAAA;AAGhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AA2BnC;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkYvE;AA6GD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG;IACjF,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB,CAcA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,KAAK,GAAG,SAAS,CAAA;IAC5B,YAAY,EAAE,KAAK,CAAA;CACpB,GAAG,OAAO,CAQV"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../../../src/dsh-adapter/plugin.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,KAAK,EAAe,MAAM,wBAAwB,CAAA;AAGhE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,qBAAqB,CAAA;AAClD,OAAO,EAAE,MAAM,EAAE,MAAM,YAAY,CAAA;AA6BnC;;;;;;;;GAQG;AACH,wBAAsB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CA2bvE;AA6GD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IAAE,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;CAAE,GAAG;IACjF,UAAU,EAAE,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC,YAAY,EAAE,MAAM,IAAI,CAAA;CACzB,CAcA;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE;IACpC,YAAY,EAAE,MAAM,CAAA;IACpB,SAAS,EAAE,KAAK,GAAG,SAAS,CAAA;IAC5B,YAAY,EAAE,KAAK,CAAA;CACpB,GAAG,OAAO,CAQV"}