linkshell-cli 0.5.19 → 0.5.21
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/dist/cli/src/runtime/acp/agent-workspace.js +64 -4
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +21 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +138 -14
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/runtime/acp/agent-workspace.ts +69 -4
- package/src/runtime/acp/claude-sdk-client.ts +143 -13
- package/web/assets/{index-Cxv9bt2l.css → index-BF_KJjhI.css} +1 -1
- package/web/assets/{index-yxFH4TkI.js → index-C6jrt3Ma.js} +13 -13
- package/web/index.html +2 -2
|
@@ -708,6 +708,8 @@ const CLAUDE_REMOTE_HIDDEN_COMMANDS = new Set([
|
|
|
708
708
|
"resume",
|
|
709
709
|
"rewind",
|
|
710
710
|
"settings",
|
|
711
|
+
"setup-bedrock",
|
|
712
|
+
"setup-vertex",
|
|
711
713
|
"statusline",
|
|
712
714
|
"stickers",
|
|
713
715
|
"tasks",
|
|
@@ -757,6 +759,7 @@ const CLAUDE_BUILT_IN_COMMANDS = [
|
|
|
757
759
|
{ name: "bug", description: "Alias for /feedback" },
|
|
758
760
|
{ name: "fewer-permission-prompts", description: "Reduce common permission prompts", argsMode: "none" },
|
|
759
761
|
{ name: "focus", description: "Toggle the focus view", argsMode: "none" },
|
|
762
|
+
{ name: "goal", description: "Set or clear a session goal the agent works toward", argsMode: "raw" },
|
|
760
763
|
{ name: "heapdump", description: "Write a JavaScript heap snapshot for diagnostics", argsMode: "none" },
|
|
761
764
|
{ name: "help", description: "Show help and available commands", argsMode: "none" },
|
|
762
765
|
{ name: "hooks", description: "View hook configurations", argsMode: "none" },
|
|
@@ -977,7 +980,20 @@ function mergeCommands(...groups) {
|
|
|
977
980
|
map.set(key, {
|
|
978
981
|
...existing,
|
|
979
982
|
...command,
|
|
983
|
+
// Preserve curated metadata the discovered set doesn't carry: the SDK's
|
|
984
|
+
// SlashCommand has no destructive/category/argsMode/executionKind
|
|
985
|
+
// fields, and runtimeCommands hardcodes argsMode:"raw" + executionKind:
|
|
986
|
+
// "prompt". The merge order is (curated, discovered), so without these
|
|
987
|
+
// a discovered /clear would clobber the curated destructive flag
|
|
988
|
+
// (red-dot warning), the grouping, AND the curated argsMode:"none" that
|
|
989
|
+
// makes the web palette execute it on pick (vs insert-and-wait). For
|
|
990
|
+
// Codex it would also flip native commands to "prompt". Curated wins on
|
|
991
|
+
// collision; genuinely new discovered commands fall through to "raw".
|
|
992
|
+
destructive: command.destructive ?? existing?.destructive,
|
|
993
|
+
category: command.category ?? existing?.category,
|
|
980
994
|
disabledReason: command.disabledReason ?? existing?.disabledReason,
|
|
995
|
+
argsMode: existing?.argsMode ?? command.argsMode,
|
|
996
|
+
executionKind: existing?.executionKind ?? command.executionKind,
|
|
981
997
|
});
|
|
982
998
|
}
|
|
983
999
|
}
|
|
@@ -1023,7 +1039,7 @@ function runtimeCommands(provider, value) {
|
|
|
1023
1039
|
})
|
|
1024
1040
|
.filter((entry) => Boolean(entry));
|
|
1025
1041
|
}
|
|
1026
|
-
function parseModelListCapabilities(value) {
|
|
1042
|
+
function parseModelListCapabilities(provider, value) {
|
|
1027
1043
|
const raw = asRecord(value);
|
|
1028
1044
|
const modelsValue = Array.isArray(value) ? value :
|
|
1029
1045
|
Array.isArray(raw?.data) ? raw.data :
|
|
@@ -1076,12 +1092,14 @@ function parseModelListCapabilities(value) {
|
|
|
1076
1092
|
const reasoningEfforts = efforts.size
|
|
1077
1093
|
? ALL_REASONING_EFFORTS.filter((effort) => efforts.has(effort))
|
|
1078
1094
|
: undefined;
|
|
1079
|
-
|
|
1095
|
+
const commands = raw && Array.isArray(raw.commands) ? runtimeCommands(provider, raw.commands) : undefined;
|
|
1096
|
+
if (models.length === 0 && !defaultModel && !reasoningEfforts?.length && !commands?.length)
|
|
1080
1097
|
return undefined;
|
|
1081
1098
|
return {
|
|
1082
1099
|
...(models.length > 0 ? { models } : {}),
|
|
1083
1100
|
...(defaultModel ? { defaultModel } : {}),
|
|
1084
1101
|
...(reasoningEfforts?.length ? { reasoningEfforts } : {}),
|
|
1102
|
+
...(commands?.length ? { commands } : {}),
|
|
1085
1103
|
};
|
|
1086
1104
|
}
|
|
1087
1105
|
function parseTimestamp(value) {
|
|
@@ -1115,6 +1133,11 @@ function parseRemoteSessions(value) {
|
|
|
1115
1133
|
const id = firstString(source, ["id", "threadId", "sessionId", "agentSessionId"]);
|
|
1116
1134
|
if (!id)
|
|
1117
1135
|
continue;
|
|
1136
|
+
// Only accept a transcript-derived status the provider actually reported
|
|
1137
|
+
// (Claude's listSessions sets "running"/"idle" from the on-disk transcript).
|
|
1138
|
+
// Anything else is left undefined so the sync site keeps its own status.
|
|
1139
|
+
const reportedStatus = firstString(source, ["status"]);
|
|
1140
|
+
const status = reportedStatus === "running" || reportedStatus === "idle" ? reportedStatus : undefined;
|
|
1118
1141
|
result.push({
|
|
1119
1142
|
id,
|
|
1120
1143
|
cwd: firstString(source, ["cwd", "workingDirectory", "workspacePath"]),
|
|
@@ -1123,6 +1146,7 @@ function parseRemoteSessions(value) {
|
|
|
1123
1146
|
createdAt: parseTimestamp(source.createdAt ?? source.created_at),
|
|
1124
1147
|
lastActivityAt: parseTimestamp(source.lastActivityAt ?? source.updatedAt ?? source.modifiedAt ?? source.lastModified ?? source.updated_at),
|
|
1125
1148
|
usage: source.usage,
|
|
1149
|
+
...(status ? { status } : {}),
|
|
1126
1150
|
});
|
|
1127
1151
|
}
|
|
1128
1152
|
return result;
|
|
@@ -1687,7 +1711,7 @@ export class AgentWorkspaceProxy {
|
|
|
1687
1711
|
if (typeof listModels === "function") {
|
|
1688
1712
|
try {
|
|
1689
1713
|
const result = await listModels.call(client);
|
|
1690
|
-
runtimeCapabilities = parseModelListCapabilities(result);
|
|
1714
|
+
runtimeCapabilities = parseModelListCapabilities(provider, result);
|
|
1691
1715
|
}
|
|
1692
1716
|
catch (error) {
|
|
1693
1717
|
if (this.input.verbose) {
|
|
@@ -1750,6 +1774,26 @@ export class AgentWorkspaceProxy {
|
|
|
1750
1774
|
const usage = usageFields
|
|
1751
1775
|
? { ...usageFields, updatedAt: remote.lastActivityAt ?? now }
|
|
1752
1776
|
: existing?.usage;
|
|
1777
|
+
// Status precedence. While LinkShell is driving a turn for this
|
|
1778
|
+
// conversation, the in-memory status is authoritative — the transcript
|
|
1779
|
+
// lags (the SDK hasn't flushed the in-flight turn to disk), so trusting
|
|
1780
|
+
// it would flicker a live "running" turn back to idle. Otherwise the
|
|
1781
|
+
// transcript-derived remote.status is authoritative for Claude (it
|
|
1782
|
+
// reports running/idle straight from disk): surface "running" while an
|
|
1783
|
+
// external process drives it, and let its "idle" clear a previously
|
|
1784
|
+
// observed "running" once the external turn finishes. We only fall
|
|
1785
|
+
// back to the existing status to keep error/waiting_permission sticky
|
|
1786
|
+
// across a re-sync — deriveTranscriptStatus never emits those, so they
|
|
1787
|
+
// live only in `existing`; defaulting everything else to idle avoids a
|
|
1788
|
+
// purely-external session getting stuck at "running" forever.
|
|
1789
|
+
const linkshellDriving = this.currentTurnIds.has(conversationId);
|
|
1790
|
+
const status = linkshellDriving
|
|
1791
|
+
? existing?.status ?? "running"
|
|
1792
|
+
: remote.status === "running"
|
|
1793
|
+
? "running"
|
|
1794
|
+
: existing?.status === "error" || existing?.status === "waiting_permission"
|
|
1795
|
+
? existing.status
|
|
1796
|
+
: "idle";
|
|
1753
1797
|
const conversation = {
|
|
1754
1798
|
id: conversationId,
|
|
1755
1799
|
agentSessionId,
|
|
@@ -1760,7 +1804,7 @@ export class AgentWorkspaceProxy {
|
|
|
1760
1804
|
reasoningEffort: existing?.reasoningEffort,
|
|
1761
1805
|
permissionMode: existing?.permissionMode,
|
|
1762
1806
|
collaborationMode: existing?.collaborationMode,
|
|
1763
|
-
status
|
|
1807
|
+
status,
|
|
1764
1808
|
archived: existing?.archived ?? false,
|
|
1765
1809
|
lastMessagePreview: existing?.lastMessagePreview,
|
|
1766
1810
|
lastActivityAt: remote.lastActivityAt ?? existing?.lastActivityAt ?? now,
|
|
@@ -1854,6 +1898,22 @@ export class AgentWorkspaceProxy {
|
|
|
1854
1898
|
const model = firstString(thread, ["model", "modelId", "currentModel"]);
|
|
1855
1899
|
if (model && !conversation.model)
|
|
1856
1900
|
conversation.model = model;
|
|
1901
|
+
// Seed a scroll-back cursor for Claude, which hydrates via readSession (full
|
|
1902
|
+
// transcript) yet pages via listTurns. The timeline only keeps the last
|
|
1903
|
+
// MAX_TIMELINE_ITEMS, so anything older is dropped here but reachable via
|
|
1904
|
+
// agent.v2.history.request. Cursor = total turn count (end-exclusive): the
|
|
1905
|
+
// first page overlaps the displayed tail (harmlessly deduped by the client's
|
|
1906
|
+
// mergeTimeline) and each subsequent page walks strictly older, terminating
|
|
1907
|
+
// when the cursor reaches 0. This integer-index cursor is Claude-specific —
|
|
1908
|
+
// Codex uses an opaque string cursor captured by the listTurns branch above,
|
|
1909
|
+
// so gate strictly on the provider rather than on listTurns presence (Codex
|
|
1910
|
+
// also exposes listTurns) to avoid poisoning Codex's cursor.
|
|
1911
|
+
if (conversation.provider === "claude" &&
|
|
1912
|
+
!this.historyCursors.has(conversation.id)) {
|
|
1913
|
+
const turnCount = Array.isArray(thread?.turns) ? thread.turns.length : 0;
|
|
1914
|
+
if (turnCount > 0)
|
|
1915
|
+
this.historyCursors.set(conversation.id, String(turnCount));
|
|
1916
|
+
}
|
|
1857
1917
|
const hydratedItems = timelineItemsFromProviderThread(source, conversation.id);
|
|
1858
1918
|
if (hydratedItems.length === 0)
|
|
1859
1919
|
return;
|