@keyou007/agent-bot 0.1.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.
- package/.env.example +8 -0
- package/CHANGELOG.md +11 -0
- package/LICENSE +675 -0
- package/README.md +187 -0
- package/README.zh.md +187 -0
- package/SECURITY.md +13 -0
- package/config.example.yaml +56 -0
- package/dist/acp/AcpJsonRpcConnection.js +155 -0
- package/dist/acp/AcpJsonRpcConnection.js.map +1 -0
- package/dist/acp/AcpProcessManager.js +65 -0
- package/dist/acp/AcpProcessManager.js.map +1 -0
- package/dist/acp/AcpSessionManager.js +164 -0
- package/dist/acp/AcpSessionManager.js.map +1 -0
- package/dist/acp/acpTypes.js +10 -0
- package/dist/acp/acpTypes.js.map +1 -0
- package/dist/cli/FeishuAppConfiguration.js +349 -0
- package/dist/cli/FeishuAppConfiguration.js.map +1 -0
- package/dist/cli/FeishuAppRegistration.js +160 -0
- package/dist/cli/FeishuAppRegistration.js.map +1 -0
- package/dist/cli/Initializer.js +112 -0
- package/dist/cli/Initializer.js.map +1 -0
- package/dist/cli/LocalControlClient.js +47 -0
- package/dist/cli/LocalControlClient.js.map +1 -0
- package/dist/cli/LocalControlServer.js +66 -0
- package/dist/cli/LocalControlServer.js.map +1 -0
- package/dist/cli/SkillRegistry.js +126 -0
- package/dist/cli/SkillRegistry.js.map +1 -0
- package/dist/cli/controlProtocol.js +10 -0
- package/dist/cli/controlProtocol.js.map +1 -0
- package/dist/cli/taskChatRoute.js +23 -0
- package/dist/cli/taskChatRoute.js.map +1 -0
- package/dist/cli.js +616 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/AppServerConnection.js +131 -0
- package/dist/codex/AppServerConnection.js.map +1 -0
- package/dist/codex/CodexEventMapper.js +296 -0
- package/dist/codex/CodexEventMapper.js.map +1 -0
- package/dist/codex/CodexLocalActivityDetector.js +124 -0
- package/dist/codex/CodexLocalActivityDetector.js.map +1 -0
- package/dist/codex/CodexProcessManager.js +64 -0
- package/dist/codex/CodexProcessManager.js.map +1 -0
- package/dist/codex/CodexRuntime.js +825 -0
- package/dist/codex/CodexRuntime.js.map +1 -0
- package/dist/codex/ProjectlessWorkspace.js +74 -0
- package/dist/codex/ProjectlessWorkspace.js.map +1 -0
- package/dist/codex/protocol.js +10 -0
- package/dist/codex/protocol.js.map +1 -0
- package/dist/commands/CommandRouter.js +176 -0
- package/dist/commands/CommandRouter.js.map +1 -0
- package/dist/commands/commandTypes.js +2 -0
- package/dist/commands/commandTypes.js.map +1 -0
- package/dist/config/loadConfig.js +107 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/paths.js +32 -0
- package/dist/config/paths.js.map +1 -0
- package/dist/config/schema.js +38 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/console/ConsoleConnector.js +39 -0
- package/dist/console/ConsoleConnector.js.map +1 -0
- package/dist/console/ConsoleTurnPresenter.js +60 -0
- package/dist/console/ConsoleTurnPresenter.js.map +1 -0
- package/dist/feishu/CardRenderer.js +972 -0
- package/dist/feishu/CardRenderer.js.map +1 -0
- package/dist/feishu/CardUpdateScheduler.js +148 -0
- package/dist/feishu/CardUpdateScheduler.js.map +1 -0
- package/dist/feishu/ConsoleFeishuClient.js +18 -0
- package/dist/feishu/ConsoleFeishuClient.js.map +1 -0
- package/dist/feishu/FeishuConnector.js +257 -0
- package/dist/feishu/FeishuConnector.js.map +1 -0
- package/dist/feishu/FeishuMarkdown.js +37 -0
- package/dist/feishu/FeishuMarkdown.js.map +1 -0
- package/dist/feishu/FeishuMessageClient.js +459 -0
- package/dist/feishu/FeishuMessageClient.js.map +1 -0
- package/dist/feishu/FeishuTurnPresenter.js +425 -0
- package/dist/feishu/FeishuTurnPresenter.js.map +1 -0
- package/dist/feishu/GroupAvatarGenerator.js +86 -0
- package/dist/feishu/GroupAvatarGenerator.js.map +1 -0
- package/dist/feishu/LocalCardImage.js +13 -0
- package/dist/feishu/LocalCardImage.js.map +1 -0
- package/dist/feishu/LocalImageMarkdown.js +89 -0
- package/dist/feishu/LocalImageMarkdown.js.map +1 -0
- package/dist/feishu/contextKey.js +12 -0
- package/dist/feishu/contextKey.js.map +1 -0
- package/dist/feishu/transport.js +14 -0
- package/dist/feishu/transport.js.map +1 -0
- package/dist/feishu/types.js +2 -0
- package/dist/feishu/types.js.map +1 -0
- package/dist/index.js +213 -0
- package/dist/index.js.map +1 -0
- package/dist/logging/logger.js +22 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/presentation/OutboundRouter.js +127 -0
- package/dist/presentation/OutboundRouter.js.map +1 -0
- package/dist/presentation/TurnStateReducer.js +307 -0
- package/dist/presentation/TurnStateReducer.js.map +1 -0
- package/dist/presentation/splitMarkdown.js +341 -0
- package/dist/presentation/splitMarkdown.js.map +1 -0
- package/dist/presentation/turnViewTypes.js +2 -0
- package/dist/presentation/turnViewTypes.js.map +1 -0
- package/dist/proxy/ProxySessionController.js +2819 -0
- package/dist/proxy/ProxySessionController.js.map +1 -0
- package/dist/runtime/AcpRuntimeAdapter.js +271 -0
- package/dist/runtime/AcpRuntimeAdapter.js.map +1 -0
- package/dist/runtime/AgentRuntimeRegistry.js +18 -0
- package/dist/runtime/AgentRuntimeRegistry.js.map +1 -0
- package/dist/runtime/agentEnvironment.js +11 -0
- package/dist/runtime/agentEnvironment.js.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/startup/SessionMetadataHydrator.js +46 -0
- package/dist/startup/SessionMetadataHydrator.js.map +1 -0
- package/dist/startup/StartupNotifier.js +78 -0
- package/dist/startup/StartupNotifier.js.map +1 -0
- package/dist/startup/startFeishu.js +6 -0
- package/dist/startup/startFeishu.js.map +1 -0
- package/dist/state/StateStore.js +893 -0
- package/dist/state/StateStore.js.map +1 -0
- package/dist/state/migrations.js +129 -0
- package/dist/state/migrations.js.map +1 -0
- package/dist/supervision/SafeRestartNotifier.js +116 -0
- package/dist/supervision/SafeRestartNotifier.js.map +1 -0
- package/dist/supervision/SafeRestartScheduler.js +123 -0
- package/dist/supervision/SafeRestartScheduler.js.map +1 -0
- package/dist/supervision/replacementSupervisor.js +8 -0
- package/dist/supervision/replacementSupervisor.js.map +1 -0
- package/dist/supervision/restartPolicy.js +16 -0
- package/dist/supervision/restartPolicy.js.map +1 -0
- package/dist/supervisor.js +94 -0
- package/dist/supervisor.js.map +1 -0
- package/dist/utils/executeShellCommand.js +76 -0
- package/dist/utils/executeShellCommand.js.map +1 -0
- package/dist/utils/id.js +5 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/markdown.js +29 -0
- package/dist/utils/markdown.js.map +1 -0
- package/dist/utils/spawnCommand.js +21 -0
- package/dist/utils/spawnCommand.js.map +1 -0
- package/dist/utils/taskTitle.js +10 -0
- package/dist/utils/taskTitle.js.map +1 -0
- package/docs/technical-reference.md +294 -0
- package/docs/technical-reference.zh.md +294 -0
- package/npm-shrinkwrap.json +4525 -0
- package/package.json +87 -0
- package/skills/agent-bot/SKILL.md +141 -0
- package/skills/agent-bot/agents/openai.yaml +4 -0
- package/src/acp/AcpJsonRpcConnection.ts +212 -0
- package/src/acp/AcpProcessManager.ts +87 -0
- package/src/acp/AcpSessionManager.ts +227 -0
- package/src/acp/acpTypes.ts +99 -0
- package/src/cli/FeishuAppConfiguration.ts +473 -0
- package/src/cli/FeishuAppRegistration.ts +207 -0
- package/src/cli/Initializer.ts +159 -0
- package/src/cli/LocalControlClient.ts +48 -0
- package/src/cli/LocalControlServer.ts +67 -0
- package/src/cli/SkillRegistry.ts +151 -0
- package/src/cli/controlProtocol.ts +34 -0
- package/src/cli/taskChatRoute.ts +31 -0
- package/src/cli.ts +630 -0
- package/src/codex/AppServerConnection.ts +159 -0
- package/src/codex/CodexEventMapper.ts +351 -0
- package/src/codex/CodexLocalActivityDetector.ts +148 -0
- package/src/codex/CodexProcessManager.ts +63 -0
- package/src/codex/CodexRuntime.ts +1025 -0
- package/src/codex/ProjectlessWorkspace.ts +90 -0
- package/src/codex/protocol.ts +37 -0
- package/src/commands/CommandRouter.ts +179 -0
- package/src/commands/commandTypes.ts +28 -0
- package/src/config/loadConfig.ts +115 -0
- package/src/config/paths.ts +37 -0
- package/src/config/schema.ts +46 -0
- package/src/console/ConsoleConnector.ts +40 -0
- package/src/console/ConsoleTurnPresenter.ts +64 -0
- package/src/feishu/CardRenderer.ts +1157 -0
- package/src/feishu/CardUpdateScheduler.ts +165 -0
- package/src/feishu/ConsoleFeishuClient.ts +22 -0
- package/src/feishu/FeishuConnector.ts +277 -0
- package/src/feishu/FeishuMarkdown.ts +47 -0
- package/src/feishu/FeishuMessageClient.ts +649 -0
- package/src/feishu/FeishuTurnPresenter.ts +539 -0
- package/src/feishu/GroupAvatarGenerator.ts +102 -0
- package/src/feishu/LocalCardImage.ts +13 -0
- package/src/feishu/LocalImageMarkdown.ts +92 -0
- package/src/feishu/contextKey.ts +14 -0
- package/src/feishu/transport.ts +24 -0
- package/src/feishu/types.ts +83 -0
- package/src/index.ts +228 -0
- package/src/logging/logger.ts +28 -0
- package/src/presentation/OutboundRouter.ts +205 -0
- package/src/presentation/TurnStateReducer.ts +341 -0
- package/src/presentation/splitMarkdown.ts +358 -0
- package/src/presentation/turnViewTypes.ts +54 -0
- package/src/proxy/ProxySessionController.ts +3311 -0
- package/src/runtime/AcpRuntimeAdapter.ts +324 -0
- package/src/runtime/AgentRuntimeRegistry.ts +20 -0
- package/src/runtime/agentEnvironment.ts +14 -0
- package/src/runtime/types.ts +215 -0
- package/src/startup/SessionMetadataHydrator.ts +46 -0
- package/src/startup/StartupNotifier.ts +92 -0
- package/src/startup/startFeishu.ts +19 -0
- package/src/state/StateStore.ts +1249 -0
- package/src/state/migrations.ts +128 -0
- package/src/supervision/SafeRestartNotifier.ts +122 -0
- package/src/supervision/SafeRestartScheduler.ts +149 -0
- package/src/supervision/replacementSupervisor.ts +10 -0
- package/src/supervision/restartPolicy.ts +19 -0
- package/src/supervisor.ts +106 -0
- package/src/utils/executeShellCommand.ts +97 -0
- package/src/utils/id.ts +5 -0
- package/src/utils/markdown.ts +25 -0
- package/src/utils/spawnCommand.ts +35 -0
- package/src/utils/taskTitle.ts +9 -0
- package/tsconfig.json +19 -0
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import type { AgentEvent, ToolState } from "../runtime/types.js";
|
|
2
|
+
import type { MessageReplyTarget } from "../feishu/types.js";
|
|
3
|
+
import type { FileSummary, TurnActivity, TurnViewState } from "./turnViewTypes.js";
|
|
4
|
+
|
|
5
|
+
const MAX_TEXT = 6_000;
|
|
6
|
+
const MAX_COMPLETED_TOOLS = 20;
|
|
7
|
+
const MAX_FAILED_TOOLS = 5;
|
|
8
|
+
const MAX_FILES = 30;
|
|
9
|
+
|
|
10
|
+
export function createTurnViewState(
|
|
11
|
+
sessionId: string,
|
|
12
|
+
turnId: string,
|
|
13
|
+
startedAt: number,
|
|
14
|
+
taskTitle?: string,
|
|
15
|
+
replyTarget?: MessageReplyTarget,
|
|
16
|
+
projectCwd?: string,
|
|
17
|
+
prompt?: string,
|
|
18
|
+
): TurnViewState {
|
|
19
|
+
return {
|
|
20
|
+
sessionId,
|
|
21
|
+
turnId,
|
|
22
|
+
taskTitle,
|
|
23
|
+
prompt,
|
|
24
|
+
projectCwd,
|
|
25
|
+
replyTarget,
|
|
26
|
+
status: "starting",
|
|
27
|
+
startedAt,
|
|
28
|
+
assistantText: "",
|
|
29
|
+
plan: [],
|
|
30
|
+
activities: [],
|
|
31
|
+
totalToolCount: 0,
|
|
32
|
+
completedToolCount: 0,
|
|
33
|
+
failedToolCount: 0,
|
|
34
|
+
toolStatuses: {},
|
|
35
|
+
completedTools: [],
|
|
36
|
+
failedTools: [],
|
|
37
|
+
fileSummary: [],
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function reduceTurnEvent(state: TurnViewState, event: AgentEvent): TurnViewState {
|
|
42
|
+
if (event.sessionId !== state.sessionId || event.turnId !== state.turnId) return state;
|
|
43
|
+
|
|
44
|
+
switch (event.type) {
|
|
45
|
+
case "turn_started":
|
|
46
|
+
return { ...state, status: "running", startedAt: event.startedAt };
|
|
47
|
+
case "agent_text_delta":
|
|
48
|
+
return { ...state, assistantText: bound(`${state.assistantText}${event.text}`) };
|
|
49
|
+
case "token_usage_updated": {
|
|
50
|
+
const lastTokens = normalizeTokenCount(event.lastTokens);
|
|
51
|
+
const cumulativeTokens = normalizeTokenCount(event.cumulativeTokens);
|
|
52
|
+
const previousCumulative = state.tokenUsageCumulative;
|
|
53
|
+
const delta = previousCumulative === undefined
|
|
54
|
+
? lastTokens
|
|
55
|
+
: Math.max(0, cumulativeTokens - previousCumulative);
|
|
56
|
+
return {
|
|
57
|
+
...state,
|
|
58
|
+
totalTokens: (state.totalTokens ?? 0) + delta,
|
|
59
|
+
tokenUsageCumulative: Math.max(previousCumulative ?? 0, cumulativeTokens),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
case "progress": {
|
|
63
|
+
const activityUpdate = upsertReasoningActivity(
|
|
64
|
+
state.activities ?? [],
|
|
65
|
+
event.activityId ?? "progress",
|
|
66
|
+
event.text,
|
|
67
|
+
event.append === true,
|
|
68
|
+
event.activityId?.startsWith("commentary:") ? "assistant" : "reasoning",
|
|
69
|
+
);
|
|
70
|
+
return {
|
|
71
|
+
...state,
|
|
72
|
+
progressText: bound(event.text),
|
|
73
|
+
activities: activityUpdate.activities,
|
|
74
|
+
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
case "plan_updated":
|
|
78
|
+
return { ...state, plan: event.steps.slice(0, 30).map((step) => ({ ...step, text: bound(step.text) })) };
|
|
79
|
+
case "tool_started": {
|
|
80
|
+
const tool = withToolTiming(state, event.tool);
|
|
81
|
+
const bounded = boundTool(tool);
|
|
82
|
+
const activityUpdate = upsertToolActivity(state.activities ?? [], bounded);
|
|
83
|
+
const tracking = trackToolStatus(state, tool);
|
|
84
|
+
return {
|
|
85
|
+
...state,
|
|
86
|
+
...tracking,
|
|
87
|
+
status: "tool_running",
|
|
88
|
+
activeTool: bounded,
|
|
89
|
+
activities: activityUpdate.activities,
|
|
90
|
+
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
91
|
+
fileSummary: mergeFiles(state.fileSummary, tool.files),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
case "tool_updated":
|
|
95
|
+
return reduceToolUpdate(state, event.tool);
|
|
96
|
+
case "tool_output_delta":
|
|
97
|
+
return reduceToolOutputDelta(state, event.toolId, event.delta);
|
|
98
|
+
case "approval_requested":
|
|
99
|
+
return { ...state, status: "waiting_for_approval", approval: event.request };
|
|
100
|
+
case "approval_resolved":
|
|
101
|
+
return { ...state, status: state.activeTool ? "tool_running" : "running", approval: undefined };
|
|
102
|
+
case "turn_completed":
|
|
103
|
+
return {
|
|
104
|
+
...state,
|
|
105
|
+
status: "completed",
|
|
106
|
+
activeTool: undefined,
|
|
107
|
+
approval: undefined,
|
|
108
|
+
completedAt: state.startedAt + (event.durationMs ?? Date.now() - state.startedAt),
|
|
109
|
+
durationMs: event.durationMs ?? Date.now() - state.startedAt,
|
|
110
|
+
finalResponse: event.finalResponse,
|
|
111
|
+
};
|
|
112
|
+
case "turn_cancelled":
|
|
113
|
+
return { ...state, status: "cancelled", activeTool: undefined, approval: undefined, completedAt: Date.now() };
|
|
114
|
+
case "turn_failed":
|
|
115
|
+
return {
|
|
116
|
+
...state,
|
|
117
|
+
status: "failed",
|
|
118
|
+
activeTool: undefined,
|
|
119
|
+
approval: undefined,
|
|
120
|
+
completedAt: Date.now(),
|
|
121
|
+
error: bound(event.message),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function appendSteerMessage(state: TurnViewState, id: string, text: string): TurnViewState {
|
|
127
|
+
const normalized = text.trim();
|
|
128
|
+
if (!normalized) return state;
|
|
129
|
+
const activities = state.activities ?? [];
|
|
130
|
+
const index = activities.findIndex((activity) => activity.id === id);
|
|
131
|
+
const activityUpdate = upsertActivity(activities, index, {
|
|
132
|
+
kind: "user",
|
|
133
|
+
id,
|
|
134
|
+
text: bound(normalized),
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
...state,
|
|
138
|
+
activities: activityUpdate.activities,
|
|
139
|
+
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function reduceToolOutputDelta(state: TurnViewState, toolId: string, delta: string): TurnViewState {
|
|
144
|
+
if (!delta) return state;
|
|
145
|
+
const tool = findTool(state, toolId);
|
|
146
|
+
if (!tool || tool.status !== "running" || tool.kind !== "command") return state;
|
|
147
|
+
return reduceToolUpdate(state, {
|
|
148
|
+
...tool,
|
|
149
|
+
output: appendBoundedOutput(tool.output, delta),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function findTool(state: TurnViewState, toolId: string): ToolState | undefined {
|
|
154
|
+
if (state.activeTool?.id === toolId) return state.activeTool;
|
|
155
|
+
const activity = [...(state.activities ?? [])].reverse().find((candidate) =>
|
|
156
|
+
candidate.kind === "tool" && candidate.tool.id === toolId);
|
|
157
|
+
if (activity?.kind === "tool") return activity.tool;
|
|
158
|
+
return state.failedTools.find((tool) => tool.id === toolId)
|
|
159
|
+
?? state.completedTools.find((tool) => tool.id === toolId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function withToolTiming(state: TurnViewState, tool: ToolState): ToolState {
|
|
163
|
+
const previous = findTool(state, tool.id);
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const startedAt = tool.startedAt ?? previous?.startedAt ?? tool.completedAt ?? now;
|
|
166
|
+
return {
|
|
167
|
+
...tool,
|
|
168
|
+
startedAt,
|
|
169
|
+
completedAt: tool.status === "running" ? tool.completedAt : tool.completedAt ?? now,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendBoundedOutput(previous: string | undefined, delta: string): string {
|
|
174
|
+
const combined = `${previous ?? ""}${delta}`;
|
|
175
|
+
if (combined.length <= MAX_TEXT) return combined;
|
|
176
|
+
return `…${combined.slice(-(MAX_TEXT - 1))}`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function reduceToolUpdate(state: TurnViewState, tool: ToolState): TurnViewState {
|
|
180
|
+
tool = withToolTiming(state, tool);
|
|
181
|
+
const tracking = trackToolStatus(state, tool);
|
|
182
|
+
const bounded = boundTool(tool);
|
|
183
|
+
const activityUpdate = upsertToolActivity(state.activities ?? [], bounded);
|
|
184
|
+
const withoutCompleted = state.completedTools.filter((item) => item.id !== tool.id);
|
|
185
|
+
const withoutFailed = state.failedTools.filter((item) => item.id !== tool.id);
|
|
186
|
+
const activeTool = state.activeTool?.id === tool.id ? undefined : state.activeTool;
|
|
187
|
+
|
|
188
|
+
if (tool.status === "running") {
|
|
189
|
+
return {
|
|
190
|
+
...state,
|
|
191
|
+
...tracking,
|
|
192
|
+
status: "tool_running",
|
|
193
|
+
activeTool: bounded,
|
|
194
|
+
activities: activityUpdate.activities,
|
|
195
|
+
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
196
|
+
completedTools: withoutCompleted,
|
|
197
|
+
failedTools: withoutFailed,
|
|
198
|
+
fileSummary: mergeFiles(state.fileSummary, tool.files),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (tool.status === "failed") {
|
|
203
|
+
return {
|
|
204
|
+
...state,
|
|
205
|
+
...tracking,
|
|
206
|
+
status: activeTool ? "tool_running" : "running",
|
|
207
|
+
activeTool,
|
|
208
|
+
activities: activityUpdate.activities,
|
|
209
|
+
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
210
|
+
completedTools: withoutCompleted,
|
|
211
|
+
failedTools: [...withoutFailed, bounded].slice(-MAX_FAILED_TOOLS),
|
|
212
|
+
fileSummary: mergeFiles(state.fileSummary, tool.files),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return {
|
|
217
|
+
...state,
|
|
218
|
+
...tracking,
|
|
219
|
+
status: activeTool ? "tool_running" : "running",
|
|
220
|
+
activeTool,
|
|
221
|
+
activities: activityUpdate.activities,
|
|
222
|
+
activitiesTruncated: state.activitiesTruncated || activityUpdate.truncated,
|
|
223
|
+
failedTools: withoutFailed,
|
|
224
|
+
completedTools: [...withoutCompleted, bounded].slice(-MAX_COMPLETED_TOOLS),
|
|
225
|
+
fileSummary: mergeFiles(state.fileSummary, tool.files),
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function trackToolStatus(
|
|
230
|
+
state: TurnViewState,
|
|
231
|
+
tool: ToolState,
|
|
232
|
+
): Pick<TurnViewState, "totalToolCount" | "completedToolCount" | "failedToolCount" | "toolStatuses"> {
|
|
233
|
+
const toolStatuses = state.toolStatuses
|
|
234
|
+
? { ...state.toolStatuses }
|
|
235
|
+
: legacyToolStatuses(state);
|
|
236
|
+
const previous = toolStatuses[tool.id];
|
|
237
|
+
let totalToolCount = state.totalToolCount ?? Object.keys(toolStatuses).length;
|
|
238
|
+
let completedToolCount = state.completedToolCount
|
|
239
|
+
?? Object.values(toolStatuses).filter((status) => status === "completed").length;
|
|
240
|
+
let failedToolCount = state.failedToolCount
|
|
241
|
+
?? Object.values(toolStatuses).filter((status) => status === "failed").length;
|
|
242
|
+
|
|
243
|
+
if (previous === undefined) totalToolCount += 1;
|
|
244
|
+
if (previous !== tool.status) {
|
|
245
|
+
if (previous === "completed") completedToolCount = Math.max(0, completedToolCount - 1);
|
|
246
|
+
if (previous === "failed") failedToolCount = Math.max(0, failedToolCount - 1);
|
|
247
|
+
if (tool.status === "completed") completedToolCount += 1;
|
|
248
|
+
if (tool.status === "failed") failedToolCount += 1;
|
|
249
|
+
}
|
|
250
|
+
toolStatuses[tool.id] = tool.status;
|
|
251
|
+
return { totalToolCount, completedToolCount, failedToolCount, toolStatuses };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function legacyToolStatuses(state: TurnViewState): Record<string, ToolState["status"]> {
|
|
255
|
+
const statuses: Record<string, ToolState["status"]> = {};
|
|
256
|
+
for (const tool of state.completedTools ?? []) statuses[tool.id] = tool.status;
|
|
257
|
+
for (const tool of state.failedTools ?? []) statuses[tool.id] = tool.status;
|
|
258
|
+
for (const activity of state.activities ?? []) {
|
|
259
|
+
if (activity.kind === "tool") statuses[activity.tool.id] = activity.tool.status;
|
|
260
|
+
}
|
|
261
|
+
if (state.activeTool) statuses[state.activeTool.id] = state.activeTool.status;
|
|
262
|
+
return statuses;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function upsertReasoningActivity(
|
|
266
|
+
activities: TurnActivity[],
|
|
267
|
+
id: string,
|
|
268
|
+
text: string,
|
|
269
|
+
append: boolean,
|
|
270
|
+
kind: "assistant" | "reasoning",
|
|
271
|
+
): ActivityUpdate {
|
|
272
|
+
const index = activities.findIndex((activity) => activity.id === id);
|
|
273
|
+
const existing = index >= 0 ? activities[index] : undefined;
|
|
274
|
+
const previousText = existing?.kind === "reasoning" || existing?.kind === "assistant" ? existing.text : "";
|
|
275
|
+
const combined = append ? `${previousText}${text}` : text;
|
|
276
|
+
const next: TurnActivity = {
|
|
277
|
+
kind,
|
|
278
|
+
id,
|
|
279
|
+
text: kind === "assistant" ? combined : bound(combined),
|
|
280
|
+
};
|
|
281
|
+
return upsertActivity(activities, index, next);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function upsertToolActivity(activities: TurnActivity[], tool: ToolState): ActivityUpdate {
|
|
285
|
+
const index = activities.findIndex((activity) => activity.id === tool.id);
|
|
286
|
+
return upsertActivity(activities, index, { kind: "tool", id: tool.id, tool });
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
interface ActivityUpdate {
|
|
290
|
+
activities: TurnActivity[];
|
|
291
|
+
truncated: boolean;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function upsertActivity(activities: TurnActivity[], index: number, activity: TurnActivity): ActivityUpdate {
|
|
295
|
+
const updated = [...activities];
|
|
296
|
+
if (index >= 0) updated[index] = activity;
|
|
297
|
+
else updated.push(activity);
|
|
298
|
+
return {
|
|
299
|
+
activities: updated,
|
|
300
|
+
truncated: false,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function boundTool(tool: ToolState): ToolState {
|
|
305
|
+
return {
|
|
306
|
+
...tool,
|
|
307
|
+
title: bound(tool.title),
|
|
308
|
+
command: tool.command === undefined ? undefined : bound(tool.command),
|
|
309
|
+
output: tool.output === undefined ? undefined : bound(tool.output),
|
|
310
|
+
error: tool.error === undefined ? undefined : bound(tool.error),
|
|
311
|
+
files: tool.files?.slice(0, MAX_FILES),
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function mergeFiles(existing: FileSummary[], incoming: ToolState["files"]): FileSummary[] {
|
|
316
|
+
if (!incoming?.length) return existing;
|
|
317
|
+
const merged = new Map(existing.map((file) => [file.path, { ...file }]));
|
|
318
|
+
for (const file of incoming) {
|
|
319
|
+
const previous = merged.get(file.path);
|
|
320
|
+
merged.set(file.path, {
|
|
321
|
+
path: file.path,
|
|
322
|
+
additions: addOptional(previous?.additions, file.additions),
|
|
323
|
+
deletions: addOptional(previous?.deletions, file.deletions),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return [...merged.values()].slice(-MAX_FILES);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function addOptional(left: number | undefined, right: number | undefined): number | undefined {
|
|
330
|
+
if (left === undefined && right === undefined) return undefined;
|
|
331
|
+
return (left ?? 0) + (right ?? 0);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function bound(value: string): string {
|
|
335
|
+
if (value.length <= MAX_TEXT) return value;
|
|
336
|
+
return `${value.slice(0, MAX_TEXT - 1)}…`;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function normalizeTokenCount(value: number): number {
|
|
340
|
+
return Math.max(0, Math.round(value));
|
|
341
|
+
}
|
|
@@ -0,0 +1,358 @@
|
|
|
1
|
+
interface FenceState {
|
|
2
|
+
opener: string;
|
|
3
|
+
delimiter: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface TextRange {
|
|
7
|
+
start: number;
|
|
8
|
+
end: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function splitMarkdown(text: string, maxLength = 4_000): string[] {
|
|
12
|
+
if (maxLength < 32) throw new Error("maxLength must be at least 32");
|
|
13
|
+
if (text.length <= maxLength) return [text];
|
|
14
|
+
|
|
15
|
+
const chunks: string[] = [];
|
|
16
|
+
let current = "";
|
|
17
|
+
let fence: FenceState | undefined;
|
|
18
|
+
const lines = text.match(/[^\n]*\n|[^\n]+$/g) ?? [];
|
|
19
|
+
|
|
20
|
+
const closingSuffix = (): string => {
|
|
21
|
+
if (!fence) return "";
|
|
22
|
+
return `${current.endsWith("\n") ? "" : "\n"}${fence.delimiter}`;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const flush = (): void => {
|
|
26
|
+
if (!current) return;
|
|
27
|
+
const continuation = fence;
|
|
28
|
+
chunks.push(`${current}${closingSuffix()}`);
|
|
29
|
+
current = continuation ? `${continuation.opener}\n` : "";
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const appendPlainLine = (line: string): void => {
|
|
33
|
+
const atomicRanges = fence ? [] : markdownAtomicRanges(line);
|
|
34
|
+
let offset = 0;
|
|
35
|
+
while (offset < line.length) {
|
|
36
|
+
// Reserve the worst-case newline plus delimiter because the appended
|
|
37
|
+
// slice may change a newline-terminated buffer into a partial line.
|
|
38
|
+
const reserve = fence ? fence.delimiter.length + 1 : 0;
|
|
39
|
+
const available = maxLength - current.length - reserve;
|
|
40
|
+
if (available <= 0) {
|
|
41
|
+
flush();
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const continuationPrefix = fence ? `${fence.opener}\n` : "";
|
|
46
|
+
const hasPayload = current.length > continuationPrefix.length;
|
|
47
|
+
const remainingLength = line.length - offset;
|
|
48
|
+
if (remainingLength > available && hasPayload) {
|
|
49
|
+
// Prefer a line boundary over filling the previous chunk. Apart from
|
|
50
|
+
// producing more readable pages, this gives inline Markdown such as
|
|
51
|
+
// links and code spans the full capacity of the next chunk.
|
|
52
|
+
flush();
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let partLength = Math.min(remainingLength, available);
|
|
57
|
+
if (!fence && remainingLength > available) {
|
|
58
|
+
partLength = findSafeMarkdownSplit(line, offset, available, atomicRanges);
|
|
59
|
+
// A Markdown construct may itself be larger than the message limit.
|
|
60
|
+
// It cannot be kept intact, so retain the lossless hard-split fallback.
|
|
61
|
+
if (partLength <= 0) partLength = available;
|
|
62
|
+
}
|
|
63
|
+
const part = line.slice(offset, offset + partLength);
|
|
64
|
+
current += part;
|
|
65
|
+
offset += part.length;
|
|
66
|
+
if (offset < line.length) flush();
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const appendTable = (tableLines: string[]): void => {
|
|
71
|
+
const header = tableLines[0];
|
|
72
|
+
const delimiter = tableLines[1];
|
|
73
|
+
if (!header || !delimiter) return;
|
|
74
|
+
const continuationPrefix = `${header}${delimiter}`;
|
|
75
|
+
const firstRow = tableLines[2];
|
|
76
|
+
const headerLength = header.length + delimiter.length;
|
|
77
|
+
const initialTableLength = firstRow && headerLength + firstRow.length <= maxLength
|
|
78
|
+
? headerLength + firstRow.length
|
|
79
|
+
: headerLength;
|
|
80
|
+
|
|
81
|
+
if (current && initialTableLength <= maxLength && current.length + initialTableLength > maxLength) flush();
|
|
82
|
+
if (headerLength <= maxLength) {
|
|
83
|
+
current += header;
|
|
84
|
+
current += delimiter;
|
|
85
|
+
} else {
|
|
86
|
+
appendPlainLine(header);
|
|
87
|
+
appendPlainLine(delimiter);
|
|
88
|
+
}
|
|
89
|
+
for (const row of tableLines.slice(2)) {
|
|
90
|
+
if (current.length + row.length <= maxLength) {
|
|
91
|
+
current += row;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
flush();
|
|
96
|
+
if (continuationPrefix.length + row.length <= maxLength) {
|
|
97
|
+
current = continuationPrefix;
|
|
98
|
+
current += row;
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// A single data row can itself exceed the card limit. There is no way
|
|
103
|
+
// to preserve it as one Markdown table row in that case, so retain the
|
|
104
|
+
// existing lossless hard-split fallback.
|
|
105
|
+
current = continuationPrefix;
|
|
106
|
+
appendPlainLine(row);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
111
|
+
const line = lines[index]!;
|
|
112
|
+
if (!fence && isTableHeader(line, lines[index + 1])) {
|
|
113
|
+
const tableLines = [line, lines[index + 1]!];
|
|
114
|
+
index += 2;
|
|
115
|
+
while (index < lines.length && isTableRow(lines[index]!)) {
|
|
116
|
+
tableLines.push(lines[index]!);
|
|
117
|
+
index += 1;
|
|
118
|
+
}
|
|
119
|
+
index -= 1;
|
|
120
|
+
appendTable(tableLines);
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const fenceLine = parseFence(line);
|
|
125
|
+
if (fenceLine) {
|
|
126
|
+
const isClosing = fence !== undefined && fenceLine.delimiter.startsWith(fence.delimiter.slice(0, 3));
|
|
127
|
+
const nextFence = isClosing ? undefined : fence ?? fenceLine;
|
|
128
|
+
const reserve = nextFence ? suffixLength(current + line, nextFence.delimiter) : 0;
|
|
129
|
+
if (current && current.length + line.length + reserve > maxLength) flush();
|
|
130
|
+
current += line;
|
|
131
|
+
fence = nextFence;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
appendPlainLine(line);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
flush();
|
|
138
|
+
return chunks;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isTableHeader(line: string, nextLine: string | undefined): boolean {
|
|
142
|
+
return isTableRow(line) && nextLine !== undefined && isTableDelimiter(nextLine);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isTableRow(line: string): boolean {
|
|
146
|
+
const value = line.trim();
|
|
147
|
+
return value.length > 0 && unescapedPipeCount(value) > 0;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function isTableDelimiter(line: string): boolean {
|
|
151
|
+
const cells = tableCells(line);
|
|
152
|
+
return cells.length > 0 && cells.every((cell) => /^:?-{3,}:?$/.test(cell.trim()));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function tableCells(line: string): string[] {
|
|
156
|
+
const value = line.trim().replace(/^\|/, "").replace(/\|$/, "");
|
|
157
|
+
const cells: string[] = [];
|
|
158
|
+
let current = "";
|
|
159
|
+
let escaped = false;
|
|
160
|
+
for (const character of value) {
|
|
161
|
+
if (character === "|" && !escaped) {
|
|
162
|
+
cells.push(current);
|
|
163
|
+
current = "";
|
|
164
|
+
} else {
|
|
165
|
+
current += character;
|
|
166
|
+
}
|
|
167
|
+
if (character === "\\" && !escaped) escaped = true;
|
|
168
|
+
else escaped = false;
|
|
169
|
+
}
|
|
170
|
+
cells.push(current);
|
|
171
|
+
return cells;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function unescapedPipeCount(value: string): number {
|
|
175
|
+
let count = 0;
|
|
176
|
+
let escaped = false;
|
|
177
|
+
for (const character of value) {
|
|
178
|
+
if (character === "|" && !escaped) count += 1;
|
|
179
|
+
if (character === "\\" && !escaped) escaped = true;
|
|
180
|
+
else escaped = false;
|
|
181
|
+
}
|
|
182
|
+
return count;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function findSafeMarkdownSplit(value: string, start: number, limit: number, ranges: TextRange[]): number {
|
|
186
|
+
if (value.length - start <= limit) return value.length - start;
|
|
187
|
+
let boundary = start + limit;
|
|
188
|
+
const activeRanges = ranges.filter((range) => range.start >= start);
|
|
189
|
+
|
|
190
|
+
for (const range of activeRanges) {
|
|
191
|
+
if (range.start < boundary && boundary < range.end) {
|
|
192
|
+
boundary = range.start;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (boundary <= start) return 0;
|
|
197
|
+
|
|
198
|
+
// Prefer a natural reading boundary, provided it is not inside another
|
|
199
|
+
// protected Markdown construct. Whitespace remains in the preceding chunk,
|
|
200
|
+
// so concatenating plain-text chunks is lossless.
|
|
201
|
+
for (let index = boundary; index > start; index -= 1) {
|
|
202
|
+
if (/\s/.test(value[index - 1]!) && isSafeBoundary(index, activeRanges)) return index - start;
|
|
203
|
+
}
|
|
204
|
+
for (let index = boundary; index > start; index -= 1) {
|
|
205
|
+
if (/[,。;:、,.!?;:]/.test(value[index - 1]!) && isSafeBoundary(index, activeRanges)) return index - start;
|
|
206
|
+
}
|
|
207
|
+
return isSafeBoundary(boundary, activeRanges) ? boundary - start : 0;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function markdownAtomicRanges(value: string): TextRange[] {
|
|
211
|
+
const ranges: TextRange[] = [];
|
|
212
|
+
collectCodeSpans(value, ranges);
|
|
213
|
+
collectLinksAndImages(value, ranges);
|
|
214
|
+
collectAngleSpans(value, ranges);
|
|
215
|
+
collectEmphasisSpans(value, ranges);
|
|
216
|
+
return mergeRanges(ranges);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function collectCodeSpans(value: string, ranges: TextRange[]): void {
|
|
220
|
+
for (let index = 0; index < value.length;) {
|
|
221
|
+
if (value[index] !== "`" || isEscaped(value, index)) {
|
|
222
|
+
index += 1;
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const delimiterLength = repeatedCharacterLength(value, index, "`");
|
|
226
|
+
const end = findClosingRun(value, index + delimiterLength, "`", delimiterLength);
|
|
227
|
+
if (end === -1) {
|
|
228
|
+
index += delimiterLength;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
ranges.push({ start: index, end: end + delimiterLength });
|
|
232
|
+
index = end + delimiterLength;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function collectLinksAndImages(value: string, ranges: TextRange[]): void {
|
|
237
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
238
|
+
if (value[index] !== "[" || isEscaped(value, index)) continue;
|
|
239
|
+
const labelEnd = findMatchingDelimiter(value, index, "[", "]");
|
|
240
|
+
if (labelEnd === -1) continue;
|
|
241
|
+
|
|
242
|
+
const start = index > 0 && value[index - 1] === "!" && !isEscaped(value, index - 1)
|
|
243
|
+
? index - 1
|
|
244
|
+
: index;
|
|
245
|
+
let end = labelEnd + 1;
|
|
246
|
+
if (value[end] === "(") {
|
|
247
|
+
const destinationEnd = findMatchingDelimiter(value, end, "(", ")");
|
|
248
|
+
if (destinationEnd !== -1) end = destinationEnd + 1;
|
|
249
|
+
} else if (value[end] === "[") {
|
|
250
|
+
const referenceEnd = findMatchingDelimiter(value, end, "[", "]");
|
|
251
|
+
if (referenceEnd !== -1) end = referenceEnd + 1;
|
|
252
|
+
}
|
|
253
|
+
ranges.push({ start, end });
|
|
254
|
+
index = end - 1;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function collectAngleSpans(value: string, ranges: TextRange[]): void {
|
|
259
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
260
|
+
if (value[index] !== "<" || isEscaped(value, index)) continue;
|
|
261
|
+
const end = findUnescaped(value, ">", index + 1);
|
|
262
|
+
if (end !== -1) {
|
|
263
|
+
ranges.push({ start: index, end: end + 1 });
|
|
264
|
+
index = end;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function collectEmphasisSpans(value: string, ranges: TextRange[]): void {
|
|
270
|
+
const delimiters = ["***", "___", "**", "__", "~~", "*", "_"];
|
|
271
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
272
|
+
if (isEscaped(value, index)) continue;
|
|
273
|
+
const delimiter = delimiters.find((candidate) => value.startsWith(candidate, index));
|
|
274
|
+
if (!delimiter || /\s/.test(value[index + delimiter.length] ?? "")) continue;
|
|
275
|
+
const end = findUnescaped(value, delimiter, index + delimiter.length, (candidate) => (
|
|
276
|
+
candidate > index + delimiter.length
|
|
277
|
+
&& !/\s/.test(value[candidate - 1] ?? "")
|
|
278
|
+
));
|
|
279
|
+
if (end === -1) continue;
|
|
280
|
+
ranges.push({ start: index, end: end + delimiter.length });
|
|
281
|
+
index = end + delimiter.length - 1;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function findMatchingDelimiter(value: string, start: number, opener: string, closer: string): number {
|
|
286
|
+
let depth = 0;
|
|
287
|
+
for (let index = start; index < value.length; index += 1) {
|
|
288
|
+
if (isEscaped(value, index)) continue;
|
|
289
|
+
if (value[index] === opener) depth += 1;
|
|
290
|
+
else if (value[index] === closer) {
|
|
291
|
+
depth -= 1;
|
|
292
|
+
if (depth === 0) return index;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
return -1;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function findClosingRun(value: string, start: number, character: string, length: number): number {
|
|
299
|
+
const delimiter = character.repeat(length);
|
|
300
|
+
return findUnescaped(value, delimiter, start, (index) => (
|
|
301
|
+
value[index - 1] !== character && value[index + length] !== character
|
|
302
|
+
));
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function findUnescaped(
|
|
306
|
+
value: string,
|
|
307
|
+
needle: string,
|
|
308
|
+
start: number,
|
|
309
|
+
accept: (index: number) => boolean = () => true,
|
|
310
|
+
): number {
|
|
311
|
+
let index = value.indexOf(needle, start);
|
|
312
|
+
while (index !== -1) {
|
|
313
|
+
if (!isEscaped(value, index) && accept(index)) return index;
|
|
314
|
+
index = value.indexOf(needle, index + 1);
|
|
315
|
+
}
|
|
316
|
+
return -1;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function repeatedCharacterLength(value: string, start: number, character: string): number {
|
|
320
|
+
let end = start;
|
|
321
|
+
while (value[end] === character) end += 1;
|
|
322
|
+
return end - start;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isEscaped(value: string, index: number): boolean {
|
|
326
|
+
let slashes = 0;
|
|
327
|
+
for (let cursor = index - 1; cursor >= 0 && value[cursor] === "\\"; cursor -= 1) slashes += 1;
|
|
328
|
+
return slashes % 2 === 1;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function mergeRanges(ranges: TextRange[]): TextRange[] {
|
|
332
|
+
const sorted = [...ranges].sort((left, right) => left.start - right.start || left.end - right.end);
|
|
333
|
+
const merged: TextRange[] = [];
|
|
334
|
+
for (const range of sorted) {
|
|
335
|
+
const previous = merged.at(-1);
|
|
336
|
+
if (!previous || range.start >= previous.end) {
|
|
337
|
+
merged.push({ ...range });
|
|
338
|
+
} else if (range.end > previous.end) {
|
|
339
|
+
previous.end = range.end;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return merged;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isSafeBoundary(index: number, ranges: TextRange[]): boolean {
|
|
346
|
+
return ranges.every((range) => index <= range.start || index >= range.end);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function parseFence(line: string): FenceState | undefined {
|
|
350
|
+
const trimmed = line.trimEnd();
|
|
351
|
+
const match = trimmed.match(/^\s*((?:`{3,})|(?:~{3,}))(.*)$/);
|
|
352
|
+
if (!match?.[1]) return undefined;
|
|
353
|
+
return { opener: trimmed.trimStart(), delimiter: match[1] };
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function suffixLength(value: string, delimiter: string): number {
|
|
357
|
+
return (value.endsWith("\n") ? 0 : 1) + delimiter.length;
|
|
358
|
+
}
|