@xmanrui/dsh-im 4.34.2 → 4.35.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +3 -1
- package/README.md +3 -1
- package/lib/client.js +1 -1
- package/lib/index.js +291 -289
- package/locale/en.json +1 -0
- package/locale/zh.json +1 -0
- package/package.json +1 -1
- package/plugin-src/host/delivery-adapter.mjs +2 -2
- package/plugin-src/host/delivery-service.mjs +13 -4
- package/plugin-src/host/index.mjs +1 -0
- package/src/channels/feishu/bridge.mjs +5 -5
- package/src/channels/feishu/feishu-cards.mjs +58 -6
- package/src/channels/feishu/feishu-runtime.mjs +9 -2
- package/src/channels/feishu/multi-bot-controller.mjs +1 -1
- package/src/channels/qq/qq-bridge.mjs +10 -8
- package/src/channels/qq/qq-mention.mjs +37 -0
- package/src/channels/qq/qq-runtime.mjs +11 -3
- package/src/channels/shared/bot-workspace-store.mjs +1 -0
- package/src/channels/shared/deferred-delivery-coordinator.mjs +2 -1
- package/src/channels/shared/deferred-delivery.mjs +8 -5
- package/src/channels/shared/harness-client.mjs +125 -13
- package/src/channels/shared/i18n-en/shared-a.mjs +6 -2
- package/src/channels/shared/message-failure.mjs +6 -2
- package/src/channels/shared/workspace-command.mjs +28 -16
|
@@ -242,10 +242,12 @@ function turnStoppedError() {
|
|
|
242
242
|
return error;
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
-
function
|
|
245
|
+
function workspaceEntries(value) {
|
|
246
246
|
if (!Array.isArray(value?.items)) return [];
|
|
247
247
|
return value.items.flatMap((item) => (
|
|
248
|
-
typeof item?.path === 'string' && isAbsolute(item.path)
|
|
248
|
+
typeof item?.path === 'string' && isAbsolute(item.path)
|
|
249
|
+
? [{ path: item.path, title: typeof item.title === 'string' ? item.title : '' }]
|
|
250
|
+
: []
|
|
249
251
|
));
|
|
250
252
|
}
|
|
251
253
|
|
|
@@ -378,6 +380,11 @@ export class AssistantTextAccumulator {
|
|
|
378
380
|
#steps = new Map();
|
|
379
381
|
#legacyText = '';
|
|
380
382
|
|
|
383
|
+
clear() {
|
|
384
|
+
this.#steps.clear();
|
|
385
|
+
this.#legacyText = '';
|
|
386
|
+
}
|
|
387
|
+
|
|
381
388
|
appendDelta(step, index, text) {
|
|
382
389
|
if (typeof text !== 'string' || !text) return;
|
|
383
390
|
const stepNumber = Number.isSafeInteger(step) ? step : 0;
|
|
@@ -413,6 +420,22 @@ export class AssistantTextAccumulator {
|
|
|
413
420
|
}
|
|
414
421
|
}
|
|
415
422
|
|
|
423
|
+
/** Keep answer text after the last tool boundary, shared by live and recovered replies. */
|
|
424
|
+
export function accumulateFinalAssistantText(accumulator, event) {
|
|
425
|
+
if (event.type === 'tool/call') {
|
|
426
|
+
accumulator.clear();
|
|
427
|
+
} else if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'text-delta') {
|
|
428
|
+
accumulator.appendDelta(event.data.step, event.data.chunk.index, event.data.chunk.text);
|
|
429
|
+
} else if (event.type === 'assistant/message') {
|
|
430
|
+
const content = event.data?.message?.content;
|
|
431
|
+
const text = textFromHarnessContent(content);
|
|
432
|
+
// Canonical tool messages can arrive before or after the separate call.
|
|
433
|
+
// An explicitly empty final message must not revive an earlier preamble.
|
|
434
|
+
if (!text || content.some((part) => part?.type === 'tool-call')) accumulator.clear();
|
|
435
|
+
else accumulator.setCanonical(event.data.step, text);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
416
439
|
function nonEmptyText(value) {
|
|
417
440
|
return typeof value === 'string' && value.trim() ? value.trim() : null;
|
|
418
441
|
}
|
|
@@ -536,6 +559,7 @@ export class HarnessReplyTracker {
|
|
|
536
559
|
#openTurn = null;
|
|
537
560
|
#targetTurn = null;
|
|
538
561
|
#assistantText = new AssistantTextAccumulator();
|
|
562
|
+
#finalAssistantText = null;
|
|
539
563
|
#latestText = '';
|
|
540
564
|
#finished = false;
|
|
541
565
|
#reason = null;
|
|
@@ -546,13 +570,14 @@ export class HarnessReplyTracker {
|
|
|
546
570
|
#pendingReasoningChars = 0;
|
|
547
571
|
#reasoning = false;
|
|
548
572
|
|
|
549
|
-
constructor({ promptRpcId, afterSeq = -1, reasoning = false }) {
|
|
573
|
+
constructor({ promptRpcId, afterSeq = -1, reasoning = false, finalAnswerOnly = false }) {
|
|
550
574
|
this.#promptRpcId = promptRpcId;
|
|
551
575
|
this.#lastSeq = afterSeq;
|
|
552
576
|
// Reasoning updates are opt-in per consumer: only channels that surface
|
|
553
577
|
// thinking traces (Telegram thinking mode) subscribe; every other channel
|
|
554
578
|
// keeps its pre-thinking-traces update stream untouched.
|
|
555
579
|
this.#reasoning = reasoning === true;
|
|
580
|
+
if (finalAnswerOnly === true) this.#finalAssistantText = new AssistantTextAccumulator();
|
|
556
581
|
}
|
|
557
582
|
|
|
558
583
|
get finished() {
|
|
@@ -568,6 +593,10 @@ export class HarnessReplyTracker {
|
|
|
568
593
|
return this.#latestText.trim();
|
|
569
594
|
}
|
|
570
595
|
|
|
596
|
+
get finalAnswer() {
|
|
597
|
+
return this.#finalAssistantText ? this.#finalAssistantText.text : this.answer;
|
|
598
|
+
}
|
|
599
|
+
|
|
571
600
|
get reason() {
|
|
572
601
|
return this.#reason;
|
|
573
602
|
}
|
|
@@ -695,6 +724,8 @@ export class HarnessReplyTracker {
|
|
|
695
724
|
}
|
|
696
725
|
if (event.data?.turn !== this.#targetTurn) continue;
|
|
697
726
|
|
|
727
|
+
if (this.#finalAssistantText) accumulateFinalAssistantText(this.#finalAssistantText, event);
|
|
728
|
+
|
|
698
729
|
if (event.type === 'assistant/chunk' && event.data?.chunk?.type === 'text-delta') {
|
|
699
730
|
const step = event.data?.step ?? 0;
|
|
700
731
|
const index = event.data.chunk.index ?? 0;
|
|
@@ -874,6 +905,14 @@ function harnessTurnSucceeded(reason) {
|
|
|
874
905
|
return (nonEmptyText(reason?.kind) ?? nonEmptyText(reason)) === 'completed';
|
|
875
906
|
}
|
|
876
907
|
|
|
908
|
+
function sessionArchivedError(sessionId, cause) {
|
|
909
|
+
const error = new Error('The Session is archived and cannot process this message.', { cause });
|
|
910
|
+
error.code = 'session-archived';
|
|
911
|
+
error.promptAccepted = false;
|
|
912
|
+
error.details = { sessionId };
|
|
913
|
+
return error;
|
|
914
|
+
}
|
|
915
|
+
|
|
877
916
|
export class HarnessClient {
|
|
878
917
|
#baseUrl;
|
|
879
918
|
#apiProxy;
|
|
@@ -1081,8 +1120,12 @@ export class HarnessClient {
|
|
|
1081
1120
|
}
|
|
1082
1121
|
|
|
1083
1122
|
async listWorkspaces(options = {}) {
|
|
1123
|
+
return (await this.listWorkspaceEntries(options)).map((item) => item.path);
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
async listWorkspaceEntries(options = {}) {
|
|
1084
1127
|
await this.ensureRunning(options);
|
|
1085
|
-
return
|
|
1128
|
+
return workspaceEntries(await this.rpc('workspace.list', {}, 30_000, options));
|
|
1086
1129
|
}
|
|
1087
1130
|
|
|
1088
1131
|
async listWorkspaceSessions(workspacePath, options = {}) {
|
|
@@ -1259,11 +1302,47 @@ export class HarnessClient {
|
|
|
1259
1302
|
}, timeoutMs, options);
|
|
1260
1303
|
}
|
|
1261
1304
|
|
|
1305
|
+
async #isSessionArchived(sessionId, options = {}) {
|
|
1306
|
+
const value = await this.rpc('workspace.list', {}, 30_000, options);
|
|
1307
|
+
if (!Array.isArray(value?.archivedSessionIds)
|
|
1308
|
+
|| value.archivedSessionIds.some((id) => typeof id !== 'string' || !id)) {
|
|
1309
|
+
throw new HarnessTransportError('harness-response-invalid', 'workspace.list');
|
|
1310
|
+
}
|
|
1311
|
+
return value.archivedSessionIds.includes(sessionId);
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// A diagnostic recheck must not hide the original failure or cancellation.
|
|
1315
|
+
async #confirmedArchiveError(sessionId, options, cause) {
|
|
1316
|
+
try {
|
|
1317
|
+
if (await this.#isSessionArchived(sessionId, options)) return sessionArchivedError(sessionId, cause);
|
|
1318
|
+
} catch (error) {
|
|
1319
|
+
if (options?.signal?.aborted) throw options.signal.reason ?? error;
|
|
1320
|
+
}
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
async #sessionRpc(method, payload, timeoutMs, options) {
|
|
1325
|
+
try {
|
|
1326
|
+
return await this.rpc(method, payload, timeoutMs, options);
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
if (options?.signal?.aborted) throw options.signal.reason ?? error;
|
|
1329
|
+
if (error instanceof HarnessRpcError
|
|
1330
|
+
&& ['internal', 'gateway/internal', 'session-not-found', 'session/not-found'].includes(error.code)) {
|
|
1331
|
+
throw await this.#confirmedArchiveError(payload.sessionId, { signal: options?.signal }, error) ?? error;
|
|
1332
|
+
}
|
|
1333
|
+
throw error;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1262
1337
|
async sessionExists(sessionId, options = {}) {
|
|
1338
|
+
// Archived Sessions still exist. Returning false here would silently bind
|
|
1339
|
+
// the conversation to a new Session; reading their active log can also fail.
|
|
1340
|
+
if (await this.#isSessionArchived(sessionId, options)) return true;
|
|
1263
1341
|
try {
|
|
1264
|
-
await this
|
|
1342
|
+
await this.#sessionRpc('session.history', { sessionId, maxMessages: 1 }, 30_000, options);
|
|
1265
1343
|
return true;
|
|
1266
1344
|
} catch (error) {
|
|
1345
|
+
if (error.code === 'session-archived') return true;
|
|
1267
1346
|
if (error instanceof HarnessRpcError && error.code === 'session-not-found') return false;
|
|
1268
1347
|
throw error;
|
|
1269
1348
|
}
|
|
@@ -1635,7 +1714,8 @@ export class HarnessClient {
|
|
|
1635
1714
|
const inboundFiles = Array.isArray(options.files) ? options.files.filter(Boolean) : [];
|
|
1636
1715
|
const inboundImages = Array.isArray(options.images) ? options.images.filter(Boolean) : [];
|
|
1637
1716
|
await this.ensureRunning({ signal });
|
|
1638
|
-
|
|
1717
|
+
if (await this.#isSessionArchived(sessionId, { signal })) throw sessionArchivedError(sessionId);
|
|
1718
|
+
const before = await this.#sessionRpc(
|
|
1639
1719
|
'session.history',
|
|
1640
1720
|
{ sessionId, maxMessages: 1 },
|
|
1641
1721
|
30_000,
|
|
@@ -1644,7 +1724,10 @@ export class HarnessClient {
|
|
|
1644
1724
|
const baselineSeq = Math.max(-1, ...(before.events ?? []).map(({ event }) => event.seq ?? -1));
|
|
1645
1725
|
const promptRpcId = `${this.#rpcIdPrefix}-${randomUUID()}`;
|
|
1646
1726
|
const releasePromptInputOrigin = registerImInputOrigin(this.#interactionRegistry, promptRpcId);
|
|
1647
|
-
const tracker = new HarnessReplyTracker({
|
|
1727
|
+
const tracker = new HarnessReplyTracker({
|
|
1728
|
+
promptRpcId, afterSeq: baselineSeq, reasoning, finalAnswerOnly: options.finalAnswerOnly,
|
|
1729
|
+
});
|
|
1730
|
+
let checkedBlockedSeq = baselineSeq;
|
|
1648
1731
|
let lastProgressAt = Date.now();
|
|
1649
1732
|
let lastPollSeq = tracker.lastSeq;
|
|
1650
1733
|
let progressTail = Promise.resolve();
|
|
@@ -1802,7 +1885,7 @@ export class HarnessClient {
|
|
|
1802
1885
|
// message that merely looks like a guidance block is not configuration.
|
|
1803
1886
|
imSourceGuidance.publish(sessionId, options.sourceGuidance);
|
|
1804
1887
|
const clientTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1805
|
-
const sendPrompt = (promptContent) => this
|
|
1888
|
+
const sendPrompt = (promptContent) => this.#sessionRpc('session.prompt', {
|
|
1806
1889
|
sessionId,
|
|
1807
1890
|
mode: 'queue',
|
|
1808
1891
|
content: promptContent,
|
|
@@ -1856,7 +1939,7 @@ export class HarnessClient {
|
|
|
1856
1939
|
// active until turn/end and can therefore outlive a stalled turn.
|
|
1857
1940
|
while (true) {
|
|
1858
1941
|
await sleep(300, signal);
|
|
1859
|
-
const history = await this
|
|
1942
|
+
const history = await this.#sessionRpc(
|
|
1860
1943
|
'session.history',
|
|
1861
1944
|
{ sessionId, maxMessages: 50 },
|
|
1862
1945
|
30_000,
|
|
@@ -1871,16 +1954,19 @@ export class HarnessClient {
|
|
|
1871
1954
|
if (tracker.finished) {
|
|
1872
1955
|
turnFinished = true;
|
|
1873
1956
|
if (!ownership?.stopRequested && !harnessTurnSucceeded(tracker.reason)) {
|
|
1874
|
-
|
|
1957
|
+
const failure = harnessTurnError(tracker.reason);
|
|
1958
|
+
if (failure.code === 'turn-blocked') {
|
|
1959
|
+
throw await this.#confirmedArchiveError(sessionId, { signal }, failure) ?? failure;
|
|
1960
|
+
}
|
|
1961
|
+
throw failure;
|
|
1875
1962
|
}
|
|
1876
1963
|
// An accepted /stop revokes attachment delivery even when Harness
|
|
1877
1964
|
// preserved a useful partial text answer for the existing UX.
|
|
1878
1965
|
const artifactCount = ownership?.stopRequested
|
|
1879
1966
|
? 0
|
|
1880
1967
|
: await deliverArtifacts();
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
}
|
|
1968
|
+
const answer = ownership?.stopRequested ? tracker.answer : tracker.finalAnswer;
|
|
1969
|
+
if (answer) return answer;
|
|
1884
1970
|
if (artifactCount > 0) return '';
|
|
1885
1971
|
if (ownership?.stopRequested) throw turnStoppedError();
|
|
1886
1972
|
if (artifactHandoffError) throw artifactHandoffError;
|
|
@@ -1892,8 +1978,30 @@ export class HarnessClient {
|
|
|
1892
1978
|
throw harnessTurnError(tracker.reason);
|
|
1893
1979
|
}
|
|
1894
1980
|
|
|
1981
|
+
if (tracker.turn === null && !ownership?.stopRequested) {
|
|
1982
|
+
// An admission gate can block before user/message supplies rpcId.
|
|
1983
|
+
// This event only prompts a Session-level archive check; it never
|
|
1984
|
+
// grants ownership of another caller's turn, artifacts or approvals.
|
|
1985
|
+
const blockedSeq = Math.max(checkedBlockedSeq, ...(history.events ?? [])
|
|
1986
|
+
.map((entry) => entry?.event ?? entry)
|
|
1987
|
+
.filter((event) => event?.type === 'turn/end'
|
|
1988
|
+
&& (event.data?.reason?.kind ?? event.data?.reason) === 'blocked'
|
|
1989
|
+
&& Number.isSafeInteger(event.seq))
|
|
1990
|
+
.map((event) => event.seq));
|
|
1991
|
+
if (blockedSeq > checkedBlockedSeq) {
|
|
1992
|
+
checkedBlockedSeq = blockedSeq;
|
|
1993
|
+
const archived = await this.#confirmedArchiveError(sessionId, { signal });
|
|
1994
|
+
if (archived) throw archived;
|
|
1995
|
+
}
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1895
1998
|
if (Date.now() - lastProgressAt < timeoutMs) continue;
|
|
1896
1999
|
|
|
2000
|
+
if (!ownership?.stopRequested) {
|
|
2001
|
+
const archived = await this.#confirmedArchiveError(sessionId, { signal });
|
|
2002
|
+
if (archived) throw archived;
|
|
2003
|
+
}
|
|
2004
|
+
|
|
1897
2005
|
let running = false;
|
|
1898
2006
|
try {
|
|
1899
2007
|
running = await this.isSessionRunning(sessionId, { signal });
|
|
@@ -1911,6 +2019,10 @@ export class HarnessClient {
|
|
|
1911
2019
|
throw timeoutError;
|
|
1912
2020
|
}
|
|
1913
2021
|
} catch (error) {
|
|
2022
|
+
if (error.code === 'session-archived') {
|
|
2023
|
+
error.promptAccepted = promptAccepted;
|
|
2024
|
+
error.details = { ...error.details, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq };
|
|
2025
|
+
}
|
|
1914
2026
|
if (error instanceof HarnessTurnError) {
|
|
1915
2027
|
error.details ??= {
|
|
1916
2028
|
sessionId, promptRpcId, baselineSeq, turn: tracker.turn, lastSeq: tracker.lastSeq,
|
|
@@ -78,8 +78,12 @@ export default {
|
|
|
78
78
|
'The model rejected the current content. Revise the request and try again.',
|
|
79
79
|
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。':
|
|
80
80
|
'The model reached its output limit without producing a displayable result. Reduce the task scope and try again.',
|
|
81
|
-
'
|
|
82
|
-
'
|
|
81
|
+
'本轮处理被阻止,未能完成。请在 DSH 中查看会话状态和相关提示。':
|
|
82
|
+
'This turn was blocked and did not finish. Check the Session status and related notices in DSH.',
|
|
83
|
+
'当前会话已归档,无法继续处理。请在 DSH 中取消归档,或发送 /new 创建新会话。':
|
|
84
|
+
'The current Session is archived and cannot continue. Unarchive it in DSH, or send /new to create a new Session.',
|
|
85
|
+
'本条消息可能仍在会话队列中,请先到 DSH 查看状态,避免重复提交。':
|
|
86
|
+
'This message may still be queued in the Session. Check its status in DSH before submitting it again.',
|
|
83
87
|
'任务被意外中断,本次未完成。请重试。':
|
|
84
88
|
'The task was unexpectedly interrupted and did not finish. Please try again.',
|
|
85
89
|
'当前会话已不存在。请发送 /new 创建新会话后重试。':
|
|
@@ -67,11 +67,13 @@ const FAILURE_MESSAGES = Object.freeze({
|
|
|
67
67
|
MODEL_OUTPUT_LIMIT:
|
|
68
68
|
'模型达到输出长度上限,但没有生成可显示的结果。请缩小任务范围后重试。',
|
|
69
69
|
TURN_BLOCKED:
|
|
70
|
-
'
|
|
70
|
+
'本轮处理被阻止,未能完成。请在 DSH 中查看会话状态和相关提示。',
|
|
71
71
|
TURN_INTERRUPTED:
|
|
72
72
|
'任务被意外中断,本次未完成。请重试。',
|
|
73
73
|
SESSION_NOT_FOUND:
|
|
74
74
|
'当前会话已不存在。请发送 /new 创建新会话后重试。',
|
|
75
|
+
SESSION_ARCHIVED:
|
|
76
|
+
'当前会话已归档,无法继续处理。请在 DSH 中取消归档,或发送 /new 创建新会话。',
|
|
75
77
|
SESSION_BUSY:
|
|
76
78
|
'当前会话仍在处理上一项任务。请等待完成,或发送 /stop 后重试。',
|
|
77
79
|
SESSION_STALE:
|
|
@@ -137,6 +139,7 @@ function failureCode(error) {
|
|
|
137
139
|
if (code === 'turn-blocked') return 'TURN_BLOCKED';
|
|
138
140
|
if (['turn-interrupted', 'turn-aborted'].includes(code)) return 'TURN_INTERRUPTED';
|
|
139
141
|
if (code === 'session-not-found') return 'SESSION_NOT_FOUND';
|
|
142
|
+
if (code === 'session-archived') return 'SESSION_ARCHIVED';
|
|
140
143
|
if (code === 'agent-busy') return 'SESSION_BUSY';
|
|
141
144
|
if (code === 'workspace-session-stale') return 'SESSION_STALE';
|
|
142
145
|
if (code.startsWith('workspace-')) return 'WORKSPACE_UNAVAILABLE';
|
|
@@ -220,7 +223,8 @@ export function classifyMessageFailure(error, {
|
|
|
220
223
|
reason: safeReason ?? safeFailureReason(error?.code) ?? code,
|
|
221
224
|
message: typeof userMessage === 'string' && userMessage.trim()
|
|
222
225
|
? userMessage.trim()
|
|
223
|
-
: t(FAILURE_MESSAGES[code])
|
|
226
|
+
: t(FAILURE_MESSAGES[code]) + (code === 'SESSION_ARCHIVED' && error?.promptAccepted === true
|
|
227
|
+
? `\n${t('本条消息可能仍在会话队列中,请先到 DSH 查看状态,避免重复提交。')}` : ''),
|
|
224
228
|
referenceId: safeReferenceId(referenceId),
|
|
225
229
|
at: Number.isFinite(at) ? at : Date.now(),
|
|
226
230
|
});
|
|
@@ -62,17 +62,19 @@ export function parseSessionListArgument(value) {
|
|
|
62
62
|
return { selector: '', limit };
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
+
async function existingWorkspacePath(value) {
|
|
66
|
+
const workspace = normalizedWorkspacePath(value);
|
|
67
|
+
if (!workspace) return null;
|
|
68
|
+
try {
|
|
69
|
+
if (!(await stat(workspace)).isDirectory()) return null;
|
|
70
|
+
return normalizedWorkspacePath(await realpath(workspace));
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
65
76
|
async function existingWorkspacePaths(values) {
|
|
66
|
-
const checked = await Promise.all(values.map(
|
|
67
|
-
const workspace = normalizedWorkspacePath(value);
|
|
68
|
-
if (!workspace) return null;
|
|
69
|
-
try {
|
|
70
|
-
if (!(await stat(workspace)).isDirectory()) return null;
|
|
71
|
-
return normalizedWorkspacePath(await realpath(workspace));
|
|
72
|
-
} catch {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
}));
|
|
77
|
+
const checked = await Promise.all(values.map(existingWorkspacePath));
|
|
76
78
|
return [...new Set(checked.filter(Boolean))];
|
|
77
79
|
}
|
|
78
80
|
|
|
@@ -109,16 +111,26 @@ async function selectedWorkspacePath(value) {
|
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
112
|
-
export async function workspacePathSnapshot(harness, options = {}) {
|
|
113
|
-
const
|
|
114
|
+
export async function workspacePathSnapshot(harness, { includeTitles = false, ...options } = {}) {
|
|
115
|
+
const withEntries = includeTitles && typeof harness.listWorkspaceEntries === 'function';
|
|
116
|
+
const listed = await (withEntries ? harness.listWorkspaceEntries(options) : harness.listWorkspaces(options));
|
|
114
117
|
const currentValue = typeof harness?.currentWorkspace === 'function'
|
|
115
118
|
? harness.currentWorkspace()
|
|
116
119
|
: null;
|
|
117
|
-
const
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
+
const current = await existingWorkspacePath(currentValue);
|
|
121
|
+
const entries = await Promise.all((Array.isArray(listed) ? listed : []).map(async (item) => ({
|
|
122
|
+
path: await existingWorkspacePath(withEntries ? item?.path : item),
|
|
123
|
+
title: withEntries ? safeDisplayText(item?.title) : '',
|
|
124
|
+
})));
|
|
125
|
+
const paths = [...new Set([...(current ? [current] : []), ...entries.map((item) => item.path).filter(Boolean)])];
|
|
126
|
+
const titles = {};
|
|
127
|
+
if (includeTitles) {
|
|
128
|
+
for (const item of entries) {
|
|
129
|
+
if (item.path && item.title && !titles[item.path]) titles[item.path] = item.title;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
120
132
|
harness.assertWorkspaceScope?.();
|
|
121
|
-
return { current: current ?? null, paths };
|
|
133
|
+
return { current: current ?? null, paths, ...(includeTitles ? { titles } : {}) };
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
export function splitWorkspaceCommandMessage(message) {
|