agentweaver 0.1.18 → 0.1.20

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 (50) hide show
  1. package/README.md +54 -6
  2. package/dist/artifacts.js +9 -0
  3. package/dist/executors/git-commit-executor.js +24 -6
  4. package/dist/flow-state.js +3 -8
  5. package/dist/git/git-diff-parser.js +223 -0
  6. package/dist/git/git-service.js +562 -0
  7. package/dist/git/git-stage-selection.js +24 -0
  8. package/dist/git/git-status-parser.js +171 -0
  9. package/dist/git/git-types.js +1 -0
  10. package/dist/index.js +454 -108
  11. package/dist/interactive/auto-flow.js +644 -0
  12. package/dist/interactive/controller.js +489 -7
  13. package/dist/interactive/progress.js +194 -1
  14. package/dist/interactive/state.js +34 -0
  15. package/dist/interactive/web/index.js +237 -5
  16. package/dist/interactive/web/protocol.js +222 -1
  17. package/dist/interactive/web/server.js +497 -3
  18. package/dist/interactive/web/static/app.js +2462 -37
  19. package/dist/interactive/web/static/index.html +113 -11
  20. package/dist/interactive/web/static/styles.css +1 -1
  21. package/dist/interactive/web/static/styles.input.css +1383 -149
  22. package/dist/pipeline/auto-flow-blocks.js +307 -0
  23. package/dist/pipeline/auto-flow-config.js +273 -0
  24. package/dist/pipeline/auto-flow-identity.js +49 -0
  25. package/dist/pipeline/auto-flow-presets.js +52 -0
  26. package/dist/pipeline/auto-flow-resolver.js +830 -0
  27. package/dist/pipeline/auto-flow-types.js +17 -0
  28. package/dist/pipeline/context.js +1 -0
  29. package/dist/pipeline/declarative-flows.js +27 -1
  30. package/dist/pipeline/flow-specs/auto-common-guided.json +11 -0
  31. package/dist/pipeline/flow-specs/auto-golang.json +12 -1
  32. package/dist/pipeline/flow-specs/bugz/bug-analyze.json +54 -1
  33. package/dist/pipeline/flow-specs/gitlab/gitlab-diff-review.json +19 -1
  34. package/dist/pipeline/flow-specs/gitlab/gitlab-review.json +33 -1
  35. package/dist/pipeline/flow-specs/review/review-project.json +19 -1
  36. package/dist/pipeline/flow-specs/task-source/manual-jira-input.json +70 -0
  37. package/dist/pipeline/node-registry.js +9 -0
  38. package/dist/pipeline/nodes/codex-prompt-node.js +8 -1
  39. package/dist/pipeline/nodes/flow-run-node.js +5 -3
  40. package/dist/pipeline/nodes/git-status-node.js +2 -168
  41. package/dist/pipeline/nodes/manual-jira-task-input-node.js +146 -0
  42. package/dist/pipeline/nodes/opencode-prompt-node.js +8 -1
  43. package/dist/pipeline/nodes/plan-codex-node.js +8 -1
  44. package/dist/pipeline/spec-loader.js +14 -4
  45. package/dist/runtime/artifact-catalog.js +403 -0
  46. package/dist/runtime/settings.js +114 -0
  47. package/dist/scope.js +14 -4
  48. package/package.json +1 -1
  49. package/dist/pipeline/flow-specs/auto-common.json +0 -179
  50. package/dist/pipeline/flow-specs/auto-simple.json +0 -141
package/README.md CHANGED
@@ -56,7 +56,7 @@ In practice, this means you can treat an agent workflow like an engineered syste
56
56
 
57
57
  - `resume` only resumes a genuinely interrupted run and uses the saved execution state without rebuilding already completed steps
58
58
  - `continue` is intended for completed iterative cycles and starts the next iteration from the latest valid artifacts without deleting historical artifacts
59
- - `restart` is treated as a new run: the current active attempt is archived under `.agentweaver/scopes/<scope>/.artifacts/restart-archives/attempt-XXXX`, then a new active attempt is created
59
+ - `restart` is treated as a new run. For end-to-end attempt flows, the current active attempt is archived under `.agentweaver/scopes/<scope>/.artifacts/restart-archives/attempt-XXXX` before the new attempt starts; for independent single-purpose flows, restart only resets that flow's saved state and keeps existing scope artifacts available.
60
60
  - For ambiguous launches, the operator must choose the action explicitly: by confirmation in interactive mode, or with `--resume`, `--continue`, or `--restart` in non-interactive mode
61
61
  - This contract applies to `auto-common`, `auto-simple`, `auto-golang`, `instant-task`, `review-loop`, `run-go-linter-loop`, and `run-go-tests-loop`
62
62
 
@@ -92,7 +92,7 @@ The full flow-spec reference now lives in [docs/declarative-workflows.md](docs/d
92
92
 
93
93
  User-invokable built-in commands currently map to these flow specs:
94
94
 
95
- - `plan` — fetches Jira task with attachments, generates clarifying questions for the developer, collects answers, and produces design, implementation plan, and QA plan as structured JSON and markdown artifacts
95
+ - `plan` — uses a normalized task source from Jira or manual input, generates clarifying questions for the developer, collects answers, and produces design, implementation plan, and QA plan as structured JSON and markdown artifacts
96
96
  - `design-review` — performs a structured critique of the latest planning artifacts and writes a dedicated `design-review/v1` artifact; `approved_with_warnings` is treated as ready to proceed and may still produce `ready-to-merge.md`
97
97
  - `task-describe` — generates a brief task description from a Jira issue or from manual input; when Jira is provided, fetches the issue and summarizes it; otherwise accepts free-form text and analyzes the codebase to produce a richer description
98
98
  - `implement` — runs LLM-backed implementation based on previously approved design and plan artifacts; executes code changes locally in the project working directory
@@ -144,7 +144,15 @@ By default, AgentWeaver tries to open the browser after the server starts succes
144
144
 
145
145
  The Web UI serves the operator console from the same local process, including `/`, `/static/app.js`, and `/static/styles.css`. Live browser interaction uses WebSocket on `/__agentweaver/ws`. Bounded checks can use `GET /__agentweaver/health`, and shutdown is available through `POST /__agentweaver/exit` or `SIGINT`/`SIGTERM`.
146
146
 
147
- Web UI state is process-local: it exists only while the AgentWeaver process is running and is not shared with other AgentWeaver processes. The Web UI is intended to match the interactive operator workflow for flow selection, launch confirmation, routing and user-input forms, progress and logs, and interrupt handling.
147
+ Web UI session state is process-local: active flow selection, confirmations, forms, progress, and logs exist only while the AgentWeaver process is running and are not shared with other AgentWeaver processes. Visual preferences such as theme, panel sizes, and log auto-scroll are stored in the global AgentWeaver settings file at `~/.agentweaver/settings.json`, so they survive host and port changes.
148
+
149
+ ### Artifact Explorer
150
+
151
+ After a Web UI workflow completes, and also after a failed run when artifacts were written before failure, the Web UI offers the Artifact Explorer. It reads markdown artifacts from the active AgentWeaver scope, including artifacts from earlier runs in that scope; the latest workflow run is used only to choose the initial preview when possible. The browser requests artifact content through safe artifact identifiers resolved by the active catalog and registry; it does not accept arbitrary filesystem paths.
152
+
153
+ The MVP explorer previews Markdown, JSON, plain text, and diff artifacts. Diffs are rendered as text. Binary and unknown artifacts are listed with metadata and safe raw/download actions, but their inline preview is a placeholder. Large previews are bounded and marked as truncated with loaded byte and total size metadata; raw and download links can still serve the full artifact bytes with no-store cache headers and safe content types.
154
+
155
+ When Web UI credentials are configured, the same HTTP Basic auth protection applies to `/`, `/static/*`, `/__agentweaver/ws`, `/__agentweaver/exit`, and the artifact list, preview, raw, and download API routes. The MVP explorer does not support artifact editing, comments, run comparison, image previews, specialized viewers, live updates, or full-text search.
148
156
 
149
157
  ## Installation
150
158
 
@@ -292,6 +300,10 @@ agentweaver gitlab-review
292
300
  agentweaver mr-description DEMO-1234
293
301
  agentweaver run-go-tests-loop DEMO-1234
294
302
  agentweaver run-go-linter-loop DEMO-1234
303
+ agentweaver auto DEMO-1234
304
+ agentweaver auto --preset simple DEMO-1234
305
+ agentweaver auto --preset standard --dry-run-flow DEMO-1234
306
+ agentweaver auto --config backend-standard --dry-run-flow DEMO-1234
295
307
  agentweaver auto-golang DEMO-1234
296
308
  agentweaver auto-common DEMO-1234
297
309
  agentweaver auto-simple DEMO-1234
@@ -307,6 +319,7 @@ node dist/index.js plan DEMO-1234
307
319
  node dist/index.js design-review DEMO-1234
308
320
  node dist/index.js implement DEMO-1234
309
321
  node dist/index.js review DEMO-1234
322
+ node dist/index.js auto --preset standard --dry-run-flow DEMO-1234
310
323
  node dist/index.js auto-golang DEMO-1234
311
324
  node dist/index.js auto-common DEMO-1234
312
325
  ```
@@ -329,12 +342,17 @@ agentweaver doctor --json
329
342
  Notes:
330
343
 
331
344
  - `--dry` fetches required context but prints launch commands instead of running Codex/OpenCode steps
345
+ - `--dry-run-flow` applies only to `agentweaver auto`; it validates and previews the resolved flow without running workflow steps or writing resolver artifacts
346
+ - `--preset <simple|standard>` applies only to `agentweaver auto`; raw `agentweaver auto` defaults to `--preset standard`
347
+ - `--config <name>` applies only to `agentweaver auto`; it loads a saved YAML config by name
332
348
  - `--verbose` streams child process stdout/stderr in direct CLI mode
333
349
  - `--prompt <text>` appends extra instructions to the prompt
334
350
  - `--scope <name>` is supported by scope-flexible flows such as `implement`, `review`, `review-fix`, `review-loop`, `run-go-tests-loop`, `run-go-linter-loop`, `gitlab-review`, and `gitlab-diff-review`
335
351
  - `--md-lang <en|ru>` applies only to generated workflow markdown artifacts, not repository source files or committed documentation
336
352
  - `--force` only affects interactive mode: it skips loading cached summary-pane content on startup so Jira-backed flows that regenerate summary artifacts can repopulate it during the run
337
- - Jira-backed flows ask for Jira input interactively when it is omitted
353
+ - Saved auto flow configs are discovered at `.agentweaver/flow-configs/<name>.yaml` first and `~/.agentweaver/flow-configs/<name>.yaml` second; the project config wins when both exist
354
+ - Non-dry `agentweaver auto` runs write `flow-config.yaml`, `resolved-flow.json`, and `resolved-flow-summary.json` under `.agentweaver/scopes/<scopeKey>/.artifacts`
355
+ - `auto-golang`, `auto-common-guided`, `auto-common`, `auto-simple`, and configurable `auto` ask for Jira input interactively when Jira input is omitted; leave it empty to paste task text in the next step when Jira is unavailable
338
356
  - `task-describe` can also work from manual task description input without Jira
339
357
  - `gitlab-review` and `gitlab-diff-review` ask for a GitLab merge request URL interactively
340
358
  - `auto-status` and `auto-reset` currently operate on persisted state for `auto-golang`
@@ -353,6 +371,36 @@ Notes:
353
371
 
354
372
  `auto-simple` is the preserved simplified pipeline: `plan → implement → review loop`, with no planning review gate and no revise rounds. It is behaviorally equivalent to the legacy `auto-common` before the planning gate was introduced.
355
373
 
374
+ `agentweaver auto` is the configurable entrypoint in front of those built-in flows. Without flags it resolves to the standard preset, equivalent to `auto-common`. `--preset simple` resolves to the simplified flow, and `--preset standard` resolves to the design-review-gated flow. Use `--dry-run-flow` to inspect the selected source, phase order, included/skipped blocks, max-iteration settings, and artifact policy without invoking LLM executors, command executors, nested flows, state mutation, or resolver artifact writes.
375
+
376
+ Saved configs are YAML files named by command flag:
377
+
378
+ ```yaml
379
+ kind: auto-flow-config
380
+ version: 1
381
+ name: backend-standard
382
+ basePreset: standard
383
+ slots:
384
+ designReview:
385
+ blocks:
386
+ - id: review.design-loop
387
+ enabled: true
388
+ maxIterations: 3
389
+ postImplementationChecks:
390
+ blocks:
391
+ - id: checks.go.linter
392
+ enabled: true
393
+ maxIterations: 5
394
+ review:
395
+ blocks:
396
+ - id: review.loop
397
+ enabled: true
398
+ final:
399
+ blocks: []
400
+ ```
401
+
402
+ Supported presets are `simple` and `standard`. Supported slots are `designReview`, `postImplementationChecks`, `review`, and `final`. Supported block ids are `review.design-loop`, `checks.go.linter`, `checks.go.tests`, and `review.loop`. `enabled` accepts `true`, `false`, or `auto`; `maxIterations`, when present, must be a positive integer.
403
+
356
404
  ## Launch Profiles and Resume
357
405
 
358
406
  Interactive flow runs can ask for an LLM launch profile: executor plus model. That selection is persisted with resumable flow state.
@@ -497,10 +545,10 @@ Use the guided flow with:
497
545
 
498
546
  ```bash
499
547
  agentweaver auto-common-guided --help-phases
500
- agentweaver auto-common-guided --accept-playbook-draft DEMO-1234
548
+ agentweaver auto-common-guided --accept-playbook-draft [DEMO-1234]
501
549
  ```
502
550
 
503
- The workflow does not read old `playbook.json` or `playbook.md` files as fallbacks. In non-interactive runs, a missing manifest stops the workflow before planning and reports the required action: run `agentweaver playbook-init --accept-playbook-draft` first, or rerun `agentweaver auto-common-guided --accept-playbook-draft <jira>`. The `--accept-playbook-draft` flag explicitly accepts the generated playbook without interactive review and allows AgentWeaver to write the manifest-based layout. An invalid manifest stops the guided phase before the LLM prompt.
551
+ The workflow does not read old `playbook.json` or `playbook.md` files as fallbacks. In non-interactive runs, a missing manifest stops the workflow before planning and reports the required action: run `agentweaver playbook-init --accept-playbook-draft` first, or rerun `agentweaver auto-common-guided --accept-playbook-draft [<jira>]` in an interactive terminal when manual task input is needed. The `--accept-playbook-draft` flag explicitly accepts the generated playbook without interactive review and allows AgentWeaver to write the manifest-based layout. An invalid manifest stops the guided phase before the LLM prompt.
504
552
 
505
553
  To inspect whether playbook guidance participated in a run, check the generated artifacts:
506
554
 
package/dist/artifacts.js CHANGED
@@ -30,6 +30,15 @@ export function artifactManifestSidecarPath(payloadPath) {
30
30
  export function artifactIndexFile(scopeKey) {
31
31
  return scopeArtifactsFile(scopeKey, "artifact-index.json");
32
32
  }
33
+ export function flowConfigYamlFile(scopeKey) {
34
+ return scopeArtifactsFile(scopeKey, "flow-config.yaml");
35
+ }
36
+ export function resolvedFlowJsonFile(scopeKey) {
37
+ return scopeArtifactsFile(scopeKey, "resolved-flow.json");
38
+ }
39
+ export function resolvedFlowSummaryJsonFile(scopeKey) {
40
+ return scopeArtifactsFile(scopeKey, "resolved-flow-summary.json");
41
+ }
33
42
  export function taskWorkspaceDir(taskKey) {
34
43
  return scopeWorkspaceDir(taskKey);
35
44
  }
@@ -1,18 +1,36 @@
1
+ import { selectPathsNeedingGitStage, uniqueGitPaths } from "../git/git-stage-selection.js";
2
+ import { parsePorcelain } from "../git/git-status-parser.js";
1
3
  export const gitCommitExecutor = {
2
4
  kind: "git-commit",
3
5
  version: 1,
4
6
  defaultConfig: {},
5
7
  async execute(context, input) {
6
- if (input.files.length > 0) {
7
- await context.runtime.runCommand(["git", "add", ...input.files], {
8
- dryRun: context.dryRun,
9
- verbose: context.verbose,
10
- label: "git add",
11
- });
8
+ const files = uniqueGitPaths(input.files);
9
+ if (files.length > 0) {
10
+ let filesToStage = files;
11
+ if (!context.dryRun) {
12
+ const statusOutput = await context.runtime.runCommand(["git", "status", "--porcelain", "--", ...files], {
13
+ dryRun: false,
14
+ verbose: context.verbose,
15
+ label: "git status",
16
+ printFailureOutput: false,
17
+ });
18
+ filesToStage = selectPathsNeedingGitStage(files, parsePorcelain(statusOutput));
19
+ }
20
+ if (filesToStage.length > 0) {
21
+ await context.runtime.runCommand(["git", "add", "-A", "--", ...filesToStage], {
22
+ dryRun: context.dryRun,
23
+ verbose: context.verbose,
24
+ label: "git add",
25
+ });
26
+ }
12
27
  }
13
28
  const commitArgs = input.editEnabled
14
29
  ? ["git", "commit", "-e", "-m", input.message]
15
30
  : ["git", "commit", "-m", input.message];
31
+ if (files.length > 0) {
32
+ commitArgs.push("--", ...files);
33
+ }
16
34
  const output = await context.runtime.runCommand(commitArgs, {
17
35
  dryRun: context.dryRun,
18
36
  verbose: context.verbose,
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { existsSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { ensureScopeWorkspaceDir, flowStateFile } from "./artifacts.js";
4
4
  import { TaskRunnerError } from "./errors.js";
5
+ import { isContinuableParentFlowId } from "./pipeline/auto-flow-identity.js";
5
6
  import { isFlowRunResumeEnvelope } from "./pipeline/flow-run-resume.js";
6
7
  import { resolveStoredExecutionRoutingSnapshot, singleLaunchProfileExecutionRouting } from "./runtime/execution-routing.js";
7
8
  const FLOW_STATE_SCHEMA_VERSION = 3;
@@ -12,12 +13,6 @@ const CONTINUABLE_FLOW_KINDS = new Set([
12
13
  "run-go-linter-loop-flow",
13
14
  "run-go-tests-loop-flow",
14
15
  ]);
15
- const CONTINUABLE_PARENT_FLOW_IDS = new Set([
16
- "auto-common",
17
- "auto-simple",
18
- "auto-golang",
19
- "instant-task",
20
- ]);
21
16
  const CONTINUABLE_DIRECT_FLOW_IDS = new Set([
22
17
  "review-loop",
23
18
  "run-go-linter-loop",
@@ -118,7 +113,7 @@ function parseTerminationLocation(terminationReason) {
118
113
  function inferContinuationMetadata(flowId, executionState) {
119
114
  const stopLocation = parseTerminationLocation(executionState.terminationReason);
120
115
  const continueEligible = CONTINUABLE_FLOW_KINDS.has(executionState.flowKind)
121
- || (CONTINUABLE_PARENT_FLOW_IDS.has(flowId) && Boolean(stopLocation.stopPhaseId && stopLocation.stopStepId));
116
+ || (isContinuableParentFlowId(flowId) && Boolean(stopLocation.stopPhaseId && stopLocation.stopStepId));
122
117
  return {
123
118
  continueEligible,
124
119
  ...(stopLocation.stopPhaseId ? { stopPhaseId: stopLocation.stopPhaseId } : {}),
@@ -262,7 +257,7 @@ export function classifyFlowLaunchAvailability(state) {
262
257
  },
263
258
  restart: {
264
259
  available: true,
265
- reason: "Archive the active attempt and start a fresh run.",
260
+ reason: "Start a fresh run.",
266
261
  },
267
262
  };
268
263
  return availability;
@@ -0,0 +1,223 @@
1
+ function splitLines(value) {
2
+ if (!value) {
3
+ return [];
4
+ }
5
+ return value.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
6
+ }
7
+ function parseHunkHeader(line) {
8
+ const match = line.match(/^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(.*)$/);
9
+ if (!match) {
10
+ return null;
11
+ }
12
+ return {
13
+ header: line,
14
+ oldStart: Number(match[1]),
15
+ oldLines: Number(match[2] ?? "1"),
16
+ newStart: Number(match[3]),
17
+ newLines: Number(match[4] ?? "1"),
18
+ };
19
+ }
20
+ function normalizeGitPath(value) {
21
+ return value.replace(/^"?[ab]\//, "").replace(/"$/, "");
22
+ }
23
+ function parseHeaderMetadata(lines) {
24
+ const metadata = {};
25
+ for (const line of lines) {
26
+ if (line.startsWith("rename from ")) {
27
+ metadata.originalPath = normalizeGitPath(line.slice("rename from ".length));
28
+ }
29
+ }
30
+ return metadata;
31
+ }
32
+ function isBinaryDiff(lines) {
33
+ return lines.some((line) => (/^Binary files .+ and .+ differ$/.test(line)
34
+ || line === "GIT binary patch"
35
+ || line.startsWith("literal ")
36
+ || line.startsWith("delta ")));
37
+ }
38
+ function pushPairedRows(rows, deletes, adds) {
39
+ const pairedCount = Math.min(deletes.length, adds.length);
40
+ for (let index = 0; index < pairedCount; index += 1) {
41
+ const left = deletes[index];
42
+ const right = adds[index];
43
+ rows.push({
44
+ kind: "modify",
45
+ leftLineNumber: left.leftLineNumber,
46
+ rightLineNumber: right.rightLineNumber,
47
+ leftText: left.text,
48
+ rightText: right.text,
49
+ });
50
+ }
51
+ for (let index = pairedCount; index < deletes.length; index += 1) {
52
+ const row = deletes[index];
53
+ rows.push({
54
+ kind: "delete",
55
+ leftLineNumber: row.leftLineNumber,
56
+ rightLineNumber: null,
57
+ leftText: row.text,
58
+ rightText: "",
59
+ });
60
+ }
61
+ for (let index = pairedCount; index < adds.length; index += 1) {
62
+ const row = adds[index];
63
+ rows.push({
64
+ kind: "add",
65
+ leftLineNumber: null,
66
+ rightLineNumber: row.rightLineNumber,
67
+ leftText: "",
68
+ rightText: row.text,
69
+ });
70
+ }
71
+ }
72
+ function normalizeRows(rawRows) {
73
+ const rows = [];
74
+ let index = 0;
75
+ while (index < rawRows.length) {
76
+ const current = rawRows[index];
77
+ if (current.kind !== "delete") {
78
+ rows.push(current.kind === "context"
79
+ ? {
80
+ kind: "context",
81
+ leftLineNumber: current.leftLineNumber,
82
+ rightLineNumber: current.rightLineNumber,
83
+ leftText: current.text,
84
+ rightText: current.text,
85
+ }
86
+ : {
87
+ kind: "add",
88
+ leftLineNumber: null,
89
+ rightLineNumber: current.rightLineNumber,
90
+ leftText: "",
91
+ rightText: current.text,
92
+ });
93
+ index += 1;
94
+ continue;
95
+ }
96
+ const deletes = [];
97
+ while (rawRows[index]?.kind === "delete") {
98
+ deletes.push(rawRows[index]);
99
+ index += 1;
100
+ }
101
+ const adds = [];
102
+ while (rawRows[index]?.kind === "add") {
103
+ adds.push(rawRows[index]);
104
+ index += 1;
105
+ }
106
+ if (adds.length > 0) {
107
+ pushPairedRows(rows, deletes, adds);
108
+ }
109
+ else {
110
+ pushPairedRows(rows, deletes, []);
111
+ }
112
+ }
113
+ return rows;
114
+ }
115
+ export function parseGitDiffOutput(output, options) {
116
+ const lines = splitLines(output);
117
+ const metadata = parseHeaderMetadata(lines);
118
+ if (isBinaryDiff(lines)) {
119
+ return {
120
+ mode: options.mode,
121
+ path: options.path,
122
+ displayPath: options.displayPath ?? options.path,
123
+ ...(options.originalPath ?? metadata.originalPath ? { originalPath: options.originalPath ?? metadata.originalPath } : {}),
124
+ binary: true,
125
+ tooLarge: false,
126
+ empty: false,
127
+ hunks: [],
128
+ message: "Binary file diff is not displayed.",
129
+ };
130
+ }
131
+ const hunks = [];
132
+ let current = null;
133
+ let leftLine = 0;
134
+ let rightLine = 0;
135
+ function finishCurrent() {
136
+ if (!current) {
137
+ return;
138
+ }
139
+ hunks.push({
140
+ header: current.header,
141
+ oldStart: current.oldStart,
142
+ oldLines: current.oldLines,
143
+ newStart: current.newStart,
144
+ newLines: current.newLines,
145
+ rows: normalizeRows(current.rawRows),
146
+ });
147
+ current = null;
148
+ }
149
+ for (const line of lines) {
150
+ const header = parseHunkHeader(line);
151
+ if (header) {
152
+ finishCurrent();
153
+ current = { ...header, rawRows: [] };
154
+ leftLine = header.oldStart;
155
+ rightLine = header.newStart;
156
+ continue;
157
+ }
158
+ if (!current) {
159
+ continue;
160
+ }
161
+ if (line.startsWith("\")) {
162
+ continue;
163
+ }
164
+ const marker = line[0] ?? "";
165
+ const value = line.length > 0 ? line.slice(1) : "";
166
+ if (marker === " ") {
167
+ current.rawRows.push({ kind: "context", leftLineNumber: leftLine, rightLineNumber: rightLine, text: value });
168
+ leftLine += 1;
169
+ rightLine += 1;
170
+ }
171
+ else if (marker === "-") {
172
+ current.rawRows.push({ kind: "delete", leftLineNumber: leftLine, rightLineNumber: null, text: value });
173
+ leftLine += 1;
174
+ }
175
+ else if (marker === "+") {
176
+ current.rawRows.push({ kind: "add", leftLineNumber: null, rightLineNumber: rightLine, text: value });
177
+ rightLine += 1;
178
+ }
179
+ }
180
+ finishCurrent();
181
+ return {
182
+ mode: options.mode,
183
+ path: options.path,
184
+ displayPath: options.displayPath ?? options.path,
185
+ ...(options.originalPath ?? metadata.originalPath ? { originalPath: options.originalPath ?? metadata.originalPath } : {}),
186
+ binary: false,
187
+ tooLarge: false,
188
+ empty: hunks.length === 0 || hunks.every((hunk) => hunk.rows.length === 0),
189
+ hunks,
190
+ ...(hunks.length === 0 ? { message: "No diff is available for this mode." } : {}),
191
+ };
192
+ }
193
+ export function createSyntheticAddedDiff(input) {
194
+ const lines = splitLines(input.content);
195
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
196
+ lines.pop();
197
+ }
198
+ return {
199
+ mode: input.mode,
200
+ path: input.path,
201
+ displayPath: input.displayPath ?? input.path,
202
+ binary: false,
203
+ tooLarge: false,
204
+ empty: lines.length === 0,
205
+ hunks: lines.length === 0
206
+ ? []
207
+ : [{
208
+ header: `@@ -0,0 +1,${lines.length} @@`,
209
+ oldStart: 0,
210
+ oldLines: 0,
211
+ newStart: 1,
212
+ newLines: lines.length,
213
+ rows: lines.map((line, index) => ({
214
+ kind: "add",
215
+ leftLineNumber: null,
216
+ rightLineNumber: index + 1,
217
+ leftText: "",
218
+ rightText: line,
219
+ })),
220
+ }],
221
+ ...(lines.length === 0 ? { message: "Untracked file is empty." } : {}),
222
+ };
223
+ }