dsh-hashline-edit 0.1.0 → 0.1.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.md CHANGED
@@ -130,11 +130,12 @@ The plugin installs a `tools/post-execute` listener for the host's `pwsh` and `b
130
130
  | Command shape | Served as |
131
131
  | --- | --- |
132
132
  | `Get-Content <file>`, `type <file>`, `cat <file>` | the file's lines |
133
- | `Get-Content <file> \| Select-Object -First/-Last/-Skip <N>` | the bounded slice |
133
+ | `Get-Content <file> \| Select-Object -First\|-Last\|-Skip <N>` | the bounded slice |
134
+ | `Get-Content <file> \| Select-Object -Skip N -First M` (either order) | disk lines N+1 through N+M |
134
135
  | `Select-String <pattern> <file>` | the matched lines, with their line numbers |
135
136
  | `rg <pattern> <file>` (explicit single file, no switches) | the matched lines, with their line numbers |
136
137
 
137
- Alignment is strict: stdout lines are mapped to 1-based disk line numbers with a monotonic, unique match (search output's own line numbers are used as hints). Anything the view cannot prove — an unknown pipeline, a wildcard path, formatted output, a truncated capture, a failed command — passes through byte-for-byte. The hook never guesses and never serves the wrong occurrence of a duplicated line.
138
+ Alignment is strict: stdout lines are mapped to 1-based disk line numbers with a monotonic, unique match (search output's own line numbers are used as hints). Every served view is prefixed with the shared `[Bash view: <file> (lines a-b of N total)]` page header, so the positions the raw output used to name are not lost. Anything the view cannot prove — an unknown pipeline, a statement separator (`;`), a variable path (`$f`), a wildcard, formatted output, a truncated capture, a failed command — passes through byte-for-byte. The hook never guesses and never serves the wrong occurrence of a duplicated line.
138
139
 
139
140
  ## Encoding governance
140
141
 
package/lib/index.js CHANGED
@@ -2268,8 +2268,156 @@ ${escalationHintMarker("operation")}`,
2268
2268
  }
2269
2269
  };
2270
2270
 
2271
+ // src/bash-view-banner.ts
2272
+ var BASH_VIEW_LABEL = "Bash view";
2273
+
2274
+ // dsh/host-compat.ts
2275
+ import { realpath } from "node:fs/promises";
2276
+ import { resolve as resolve3 } from "node:path";
2277
+ var DEFAULT_MAX_LINES = 2e3;
2278
+ var DEFAULT_MAX_BYTES = 50 * 1024;
2279
+ function splitLinesForCounting(content) {
2280
+ if (content.length === 0) return [];
2281
+ const lines = content.split("\n");
2282
+ if (content.endsWith("\n")) lines.pop();
2283
+ return lines;
2284
+ }
2285
+ function formatSize(bytes) {
2286
+ if (bytes < 1024) return `${bytes}B`;
2287
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
2288
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
2289
+ }
2290
+ function truncateHead(content, options = {}) {
2291
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
2292
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
2293
+ const totalBytes = Buffer.byteLength(content, "utf-8");
2294
+ const lines = splitLinesForCounting(content);
2295
+ const totalLines = lines.length;
2296
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
2297
+ return {
2298
+ content,
2299
+ truncated: false,
2300
+ truncatedBy: null,
2301
+ totalLines,
2302
+ totalBytes,
2303
+ outputLines: totalLines,
2304
+ outputBytes: totalBytes,
2305
+ lastLinePartial: false,
2306
+ firstLineExceedsLimit: false,
2307
+ maxLines,
2308
+ maxBytes
2309
+ };
2310
+ }
2311
+ const firstLineBytes = Buffer.byteLength(lines[0] ?? "", "utf-8");
2312
+ if (firstLineBytes > maxBytes) {
2313
+ return {
2314
+ content: "",
2315
+ truncated: true,
2316
+ truncatedBy: "bytes",
2317
+ totalLines,
2318
+ totalBytes,
2319
+ outputLines: 0,
2320
+ outputBytes: 0,
2321
+ lastLinePartial: false,
2322
+ firstLineExceedsLimit: true,
2323
+ maxLines,
2324
+ maxBytes
2325
+ };
2326
+ }
2327
+ const outputLinesArr = [];
2328
+ let outputBytesCount = 0;
2329
+ let truncatedBy = "lines";
2330
+ for (let index = 0; index < lines.length && index < maxLines; index += 1) {
2331
+ const line = lines[index] ?? "";
2332
+ const lineBytes = Buffer.byteLength(line, "utf-8") + (index > 0 ? 1 : 0);
2333
+ if (outputBytesCount + lineBytes > maxBytes) {
2334
+ truncatedBy = "bytes";
2335
+ break;
2336
+ }
2337
+ outputLinesArr.push(line);
2338
+ outputBytesCount += lineBytes;
2339
+ }
2340
+ if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
2341
+ truncatedBy = "lines";
2342
+ }
2343
+ const outputContent = outputLinesArr.join("\n");
2344
+ return {
2345
+ content: outputContent,
2346
+ truncated: true,
2347
+ truncatedBy,
2348
+ totalLines,
2349
+ totalBytes,
2350
+ outputLines: outputLinesArr.length,
2351
+ outputBytes: Buffer.byteLength(outputContent, "utf-8"),
2352
+ lastLinePartial: false,
2353
+ firstLineExceedsLimit: false,
2354
+ maxLines,
2355
+ maxBytes
2356
+ };
2357
+ }
2358
+ var fileMutationQueues = /* @__PURE__ */ new Map();
2359
+ var registrationQueue = Promise.resolve();
2360
+ function isMissingPathError(error) {
2361
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
2362
+ }
2363
+ async function getMutationQueueKey(filePath) {
2364
+ const resolvedPath = resolve3(filePath);
2365
+ try {
2366
+ return await realpath(resolvedPath);
2367
+ } catch (error) {
2368
+ if (isMissingPathError(error)) return resolvedPath;
2369
+ throw error;
2370
+ }
2371
+ }
2372
+ async function withFileMutationQueue(filePath, run) {
2373
+ const registration = registrationQueue.then(async () => {
2374
+ const key2 = await getMutationQueueKey(filePath);
2375
+ const currentQueue2 = fileMutationQueues.get(key2) ?? Promise.resolve();
2376
+ let releaseNext2 = () => {
2377
+ };
2378
+ const nextQueue = new Promise((resolveQueue) => {
2379
+ releaseNext2 = resolveQueue;
2380
+ });
2381
+ const chainedQueue2 = currentQueue2.then(() => nextQueue);
2382
+ fileMutationQueues.set(key2, chainedQueue2);
2383
+ return { key: key2, currentQueue: currentQueue2, chainedQueue: chainedQueue2, releaseNext: releaseNext2 };
2384
+ });
2385
+ registrationQueue = registration.then(
2386
+ () => void 0,
2387
+ () => void 0
2388
+ );
2389
+ const { key, currentQueue, chainedQueue, releaseNext } = await registration;
2390
+ await currentQueue;
2391
+ try {
2392
+ return await run();
2393
+ } finally {
2394
+ releaseNext();
2395
+ if (fileMutationQueues.get(key) === chainedQueue) fileMutationQueues.delete(key);
2396
+ }
2397
+ }
2398
+
2399
+ // src/file-content/served-header.ts
2400
+ function stopSuffix(stoppedAtBytes) {
2401
+ return stoppedAtBytes === void 0 ? "" : `; stopped at ${formatSize(stoppedAtBytes)}`;
2402
+ }
2403
+ function rangeOf(shown, totalLines) {
2404
+ if (shown === void 0 || shown.startLine === 1 && shown.endLine === totalLines) {
2405
+ return `${totalLines} lines total`;
2406
+ }
2407
+ return `lines ${shown.startLine}-${shown.endLine} of ${totalLines} total`;
2408
+ }
2409
+ function formatPageHeader(parts) {
2410
+ const count = rangeOf(parts.shownRange, parts.totalLines);
2411
+ const body = parts.verbatim === true ? `verbatim, ${count}` : count;
2412
+ const label = parts.label === void 0 ? "" : `${parts.label}: `;
2413
+ return `[${label}${parts.file} (${body}${stopSuffix(parts.stoppedAtBytes)})]`;
2414
+ }
2415
+ function formatSectionHeader(startLine, endLine, totalLines, stoppedAtBytes) {
2416
+ return `[Lines ${startLine}-${endLine} of ${totalLines} total${stopSuffix(stoppedAtBytes)}]`;
2417
+ }
2418
+
2271
2419
  // src/hashline/hash-identity.ts
2272
- import { isAbsolute as isAbsolute3, resolve as resolve3 } from "node:path";
2420
+ import { isAbsolute as isAbsolute3, resolve as resolve4 } from "node:path";
2273
2421
 
2274
2422
  // src/hashline/hasher.ts
2275
2423
  import xxhash from "xxhash-wasm";
@@ -2408,7 +2556,7 @@ function canonicalAnchorPath(path) {
2408
2556
  message: `Anchor derivation requires an absolute file path, got ${JSON.stringify(path)}.`
2409
2557
  });
2410
2558
  }
2411
- return resolve3(path);
2559
+ return resolve4(path);
2412
2560
  }
2413
2561
  function contentBaseIndex(canonText) {
2414
2562
  return (xxh32(canonText) >>> 14) % HASH_SPACE;
@@ -6224,131 +6372,6 @@ async function anchorWalkFor(normalized, absolutePath, options) {
6224
6372
  });
6225
6373
  }
6226
6374
 
6227
- // dsh/host-compat.ts
6228
- import { realpath } from "node:fs/promises";
6229
- import { resolve as resolve4 } from "node:path";
6230
- var DEFAULT_MAX_LINES = 2e3;
6231
- var DEFAULT_MAX_BYTES = 50 * 1024;
6232
- function splitLinesForCounting(content) {
6233
- if (content.length === 0) return [];
6234
- const lines = content.split("\n");
6235
- if (content.endsWith("\n")) lines.pop();
6236
- return lines;
6237
- }
6238
- function formatSize(bytes) {
6239
- if (bytes < 1024) return `${bytes}B`;
6240
- if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
6241
- return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
6242
- }
6243
- function truncateHead(content, options = {}) {
6244
- const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
6245
- const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
6246
- const totalBytes = Buffer.byteLength(content, "utf-8");
6247
- const lines = splitLinesForCounting(content);
6248
- const totalLines = lines.length;
6249
- if (totalLines <= maxLines && totalBytes <= maxBytes) {
6250
- return {
6251
- content,
6252
- truncated: false,
6253
- truncatedBy: null,
6254
- totalLines,
6255
- totalBytes,
6256
- outputLines: totalLines,
6257
- outputBytes: totalBytes,
6258
- lastLinePartial: false,
6259
- firstLineExceedsLimit: false,
6260
- maxLines,
6261
- maxBytes
6262
- };
6263
- }
6264
- const firstLineBytes = Buffer.byteLength(lines[0] ?? "", "utf-8");
6265
- if (firstLineBytes > maxBytes) {
6266
- return {
6267
- content: "",
6268
- truncated: true,
6269
- truncatedBy: "bytes",
6270
- totalLines,
6271
- totalBytes,
6272
- outputLines: 0,
6273
- outputBytes: 0,
6274
- lastLinePartial: false,
6275
- firstLineExceedsLimit: true,
6276
- maxLines,
6277
- maxBytes
6278
- };
6279
- }
6280
- const outputLinesArr = [];
6281
- let outputBytesCount = 0;
6282
- let truncatedBy = "lines";
6283
- for (let index = 0; index < lines.length && index < maxLines; index += 1) {
6284
- const line = lines[index] ?? "";
6285
- const lineBytes = Buffer.byteLength(line, "utf-8") + (index > 0 ? 1 : 0);
6286
- if (outputBytesCount + lineBytes > maxBytes) {
6287
- truncatedBy = "bytes";
6288
- break;
6289
- }
6290
- outputLinesArr.push(line);
6291
- outputBytesCount += lineBytes;
6292
- }
6293
- if (outputLinesArr.length >= maxLines && outputBytesCount <= maxBytes) {
6294
- truncatedBy = "lines";
6295
- }
6296
- const outputContent = outputLinesArr.join("\n");
6297
- return {
6298
- content: outputContent,
6299
- truncated: true,
6300
- truncatedBy,
6301
- totalLines,
6302
- totalBytes,
6303
- outputLines: outputLinesArr.length,
6304
- outputBytes: Buffer.byteLength(outputContent, "utf-8"),
6305
- lastLinePartial: false,
6306
- firstLineExceedsLimit: false,
6307
- maxLines,
6308
- maxBytes
6309
- };
6310
- }
6311
- var fileMutationQueues = /* @__PURE__ */ new Map();
6312
- var registrationQueue = Promise.resolve();
6313
- function isMissingPathError(error) {
6314
- return typeof error === "object" && error !== null && "code" in error && (error.code === "ENOENT" || error.code === "ENOTDIR");
6315
- }
6316
- async function getMutationQueueKey(filePath) {
6317
- const resolvedPath = resolve4(filePath);
6318
- try {
6319
- return await realpath(resolvedPath);
6320
- } catch (error) {
6321
- if (isMissingPathError(error)) return resolvedPath;
6322
- throw error;
6323
- }
6324
- }
6325
- async function withFileMutationQueue(filePath, run) {
6326
- const registration = registrationQueue.then(async () => {
6327
- const key2 = await getMutationQueueKey(filePath);
6328
- const currentQueue2 = fileMutationQueues.get(key2) ?? Promise.resolve();
6329
- let releaseNext2 = () => {
6330
- };
6331
- const nextQueue = new Promise((resolveQueue) => {
6332
- releaseNext2 = resolveQueue;
6333
- });
6334
- const chainedQueue2 = currentQueue2.then(() => nextQueue);
6335
- fileMutationQueues.set(key2, chainedQueue2);
6336
- return { key: key2, currentQueue: currentQueue2, chainedQueue: chainedQueue2, releaseNext: releaseNext2 };
6337
- });
6338
- registrationQueue = registration.then(
6339
- () => void 0,
6340
- () => void 0
6341
- );
6342
- const { key, currentQueue, chainedQueue, releaseNext } = await registration;
6343
- await currentQueue;
6344
- try {
6345
- return await run();
6346
- } finally {
6347
- releaseNext();
6348
- if (fileMutationQueues.get(key) === chainedQueue) fileMutationQueues.delete(key);
6349
- }
6350
- }
6351
-
6352
6375
  // src/served-session/serve-render.ts
6353
6376
  function buildRangeServeRows(startLine, endLine, fileHashes) {
6354
6377
  const total = endLine - startLine + 1;
@@ -7517,14 +7540,6 @@ function changedRange(original, result) {
7517
7540
  };
7518
7541
  }
7519
7542
 
7520
- // src/file-content/served-header.ts
7521
- function stopSuffix(stoppedAtBytes) {
7522
- return stoppedAtBytes === void 0 ? "" : `; stopped at ${formatSize(stoppedAtBytes)}`;
7523
- }
7524
- function formatSectionHeader(startLine, endLine, totalLines, stoppedAtBytes) {
7525
- return `[Lines ${startLine}-${endLine} of ${totalLines} total${stopSuffix(stoppedAtBytes)}]`;
7526
- }
7527
-
7528
7543
  // src/file-content/preview.ts
7529
7544
  function normPosInt(value, name2) {
7530
7545
  if (value === void 0) {
@@ -10861,6 +10876,7 @@ function usablePath(value) {
10861
10876
  if (value === void 0) return void 0;
10862
10877
  const path = value.trim();
10863
10878
  if (path.length === 0 || path.startsWith("-")) return void 0;
10879
+ if (path.startsWith("$")) return void 0;
10864
10880
  if (/[*?[\]]/u.test(path)) return void 0;
10865
10881
  return path;
10866
10882
  }
@@ -10922,26 +10938,37 @@ function aliasPath(stage) {
10922
10938
  if (stage.length !== 2) return void 0;
10923
10939
  return usablePath(stage[1]);
10924
10940
  }
10925
- function selectObjectBound(stage) {
10941
+ function selectObjectBounds(stage) {
10926
10942
  if (stage[0] === void 0 || !eq(stage[0], "select-object")) return void 0;
10927
- let bound;
10943
+ const seen = {};
10928
10944
  for (let index = 1; index < stage.length; index += 1) {
10929
10945
  const token = stage[index];
10930
- if (token === void 0 || bound !== void 0) return void 0;
10946
+ if (token === void 0) return void 0;
10931
10947
  const lower = token.toLowerCase();
10932
- let kind;
10933
- if (lower === "-first") kind = "first";
10934
- else if (lower === "-last") kind = "last";
10935
- else if (lower === "-skip") kind = "skip";
10948
+ let key;
10949
+ if (lower === "-first") key = "first";
10950
+ else if (lower === "-last") key = "last";
10951
+ else if (lower === "-skip") key = "skip";
10936
10952
  else return void 0;
10937
10953
  const raw = stage[index + 1];
10938
10954
  index += 1;
10939
10955
  if (raw === void 0 || !/^\d+$/u.test(raw)) return void 0;
10956
+ if (seen[key] !== void 0) return void 0;
10940
10957
  const count = Number(raw);
10941
10958
  if (!Number.isSafeInteger(count)) return void 0;
10942
- bound = { kind, count };
10959
+ seen[key] = count;
10960
+ }
10961
+ const skip = seen.skip;
10962
+ const firstBound = seen.first;
10963
+ const lastBound = seen.last;
10964
+ if (lastBound !== void 0 && (skip !== void 0 || firstBound !== void 0)) {
10965
+ return void 0;
10943
10966
  }
10944
- return bound;
10967
+ if (skip !== void 0 && firstBound !== void 0) return { skip, first: firstBound };
10968
+ if (skip !== void 0) return { skip };
10969
+ if (firstBound !== void 0) return { first: firstBound };
10970
+ if (lastBound !== void 0) return { last: lastBound };
10971
+ return void 0;
10945
10972
  }
10946
10973
  function selectStringPath(stage) {
10947
10974
  if (stage[0] === void 0 || !eq(stage[0], "select-string")) return void 0;
@@ -11030,9 +11057,23 @@ function parse2(command) {
11030
11057
  const second = stages[1];
11031
11058
  if (stages.length !== 2 || second === void 0 || !eq(head, "get-content")) return void 0;
11032
11059
  const path = getContentPath(first);
11033
- const bound = selectObjectBound(second);
11034
- if (path === void 0 || bound === void 0) return void 0;
11035
- return { kind: bound.kind, path, count: bound.count };
11060
+ const bounds = selectObjectBounds(second);
11061
+ if (path === void 0 || bounds === void 0) return void 0;
11062
+ const firstBound = bounds.first;
11063
+ const lastBound = bounds.last;
11064
+ if (bounds.skip !== void 0 && firstBound !== void 0) {
11065
+ return { kind: "window", path, skip: bounds.skip, count: firstBound };
11066
+ }
11067
+ if (bounds.skip !== void 0 && firstBound === void 0 && lastBound === void 0) {
11068
+ return { kind: "skip", path, count: bounds.skip };
11069
+ }
11070
+ if (firstBound !== void 0 && bounds.skip === void 0 && lastBound === void 0) {
11071
+ return { kind: "first", path, count: firstBound };
11072
+ }
11073
+ if (lastBound !== void 0 && bounds.skip === void 0 && firstBound === void 0) {
11074
+ return { kind: "last", path, count: lastBound };
11075
+ }
11076
+ return void 0;
11036
11077
  }
11037
11078
  function firstGreater(sorted, after) {
11038
11079
  let low = 0;
@@ -11148,6 +11189,11 @@ function alignTransform(command, fileLines, stdoutLines) {
11148
11189
  return stdoutLines.length === total ? aligned : void 0;
11149
11190
  }
11150
11191
  if (command.kind === "search") return aligned;
11192
+ if (command.kind === "window") {
11193
+ const window = Math.min(command.count, Math.max(0, total - command.skip));
11194
+ if (stdoutLines.length !== window) return void 0;
11195
+ return aligned[0] === command.skip + 1 ? aligned : void 0;
11196
+ }
11151
11197
  const expected = command.kind === "skip" ? Math.max(0, total - command.count) : Math.min(command.count, total);
11152
11198
  if (stdoutLines.length !== expected) return void 0;
11153
11199
  if (command.kind === "first") return aligned[0] === 1 ? aligned : void 0;
@@ -11189,7 +11235,16 @@ async function plan(deps, request) {
11189
11235
  if (row === void 0) return void 0;
11190
11236
  rows.push(`${row.hash}${HASH_SEP}${row.text}`);
11191
11237
  }
11192
- return { path: command.path, cwd: request.cwd, lineNumbers, rows };
11238
+ const startLine = lineNumbers[0];
11239
+ const endLine = lineNumbers[lineNumbers.length - 1];
11240
+ if (startLine === void 0 || endLine === void 0) return void 0;
11241
+ const header = formatPageHeader({
11242
+ label: BASH_VIEW_LABEL,
11243
+ file: command.path,
11244
+ totalLines: fileLines.length,
11245
+ shownRange: { startLine, endLine }
11246
+ });
11247
+ return { path: command.path, cwd: request.cwd, lineNumbers, header, rows };
11193
11248
  } catch (error) {
11194
11249
  reportDiagnostic("shell-view", "Could not serve the shell view; passing through:", error);
11195
11250
  return void 0;
@@ -11284,7 +11339,8 @@ function createShellHooks(deps) {
11284
11339
  return decision;
11285
11340
  }
11286
11341
  if (planned === void 0 || planned.rows.length === 0) return decision;
11287
- const anchoredText = `${planned.rows.join("\n")}
11342
+ const anchoredText = `${planned.header}
11343
+ ${planned.rows.join("\n")}
11288
11344
  `;
11289
11345
  deps.metrics?.record({
11290
11346
  tool: "shell_view",
@@ -4,13 +4,14 @@
4
4
  *
5
5
  * Three steps, each fail-closed:
6
6
  * - {@link parse} admits only commands whose output is the file's own lines
7
- * (`Get-Content`/`type`/`cat`, an optional `Select-Object -First/-Last/-Skip`
8
- * bound, `Select-String`, and `rg` with an explicit single file);
7
+ * (`Get-Content`/`type`/`cat`, a `Select-Object -First|-Last|-Skip` bound or
8
+ * the `-Skip N -First M` window, `Select-String`, and `rg` with one explicit file);
9
9
  * - {@link align} maps the stdout lines back to disk lines with a monotonic
10
10
  * match that must be unique — duplicate content without a unique position is
11
11
  * ambiguous and refuses;
12
12
  * - {@link plan} serves the aligned lines through the read tool's seam (governed
13
- * decode, stat adoption, snapshot materialization, leases) and returns
13
+ * decode, stat adoption, snapshot materialization, leases), prints the shared
14
+ * `[Bash view: …]` page header naming the served range, and returns
14
15
  * `HASH│content` rows.
15
16
  *
16
17
  * Anything unmodelled — an unknown pipe, redirection, a wildcard path, formatted
@@ -24,10 +25,17 @@ export type ParsedShellCommand = {
24
25
  readonly kind: "full";
25
26
  readonly path: string;
26
27
  } | {
28
+ /** One `Select-Object` bound. */
27
29
  readonly kind: "first" | "last" | "skip";
28
30
  readonly path: string;
29
31
  /** The `-First`/`-Last`/`-Skip` argument. */
30
32
  readonly count: number;
33
+ } | {
34
+ /** `Select-Object -Skip N -First M`: disk lines N+1 through N+M. */
35
+ readonly kind: "window";
36
+ readonly path: string;
37
+ readonly skip: number;
38
+ readonly count: number;
31
39
  } | {
32
40
  readonly kind: "search";
33
41
  readonly path: string;
@@ -49,6 +57,8 @@ export interface ShellViewPlan {
49
57
  readonly cwd: string;
50
58
  /** 1-based disk line numbers, ascending. */
51
59
  readonly lineNumbers: number[];
60
+ /** The shared `[Bash view: …]` page header naming the served range. */
61
+ readonly header: string;
52
62
  /** `HASH│content` rows for those lines, in line order. */
53
63
  readonly rows: string[];
54
64
  }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * WHY: one definition site for the bash seams' bracket roots (#150, read-chrome): `view`, `search`,
3
+ * WHY: `file list` and `auto-read` each carry a kind word — the kind word IS the completeness claim —
4
+ * WHY: and that literal used to be copied at seven call sites in `src/lifecycle-hooks/index.ts`. Every
5
+ * WHY: site names its kind here instead, so the text below is the single definition site spec §3.4
6
+ * WHY: names. The `---` banner is gone: a page's chrome is its header, and only these two roots
7
+ * WHY: (a search's match list, a path list) have no page to name.
8
+ */
9
+ export declare const BASH_SEARCH_ROOT = "[Bash search (anchors)]";
10
+ export declare const BASH_FILE_LIST_ROOT = "[Bash file list (no anchors)]";
11
+ /**
12
+ * WHY: `view` and `auto-read` merged into their page headers (`[Bash view: f (40 lines total)]`), so
13
+ * WHY: their frozen text is the label, not a line of its own — the completeness claim moves into the
14
+ * WHY: header's bracket and a page can no longer carry two of them.
15
+ */
16
+ export declare const BASH_VIEW_LABEL = "Bash view";
17
+ export declare const BASH_AUTO_READ_LABEL = "Auto-read";
18
+ export type BashBannerKind = "search" | "file-list";
19
+ /** The bracket root that introduces a result with no page header of its own. */
20
+ export declare function bashBanner(kind: BashBannerKind): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-hashline-edit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Hash-anchored read/edit/undo_last_edit tools for DeepSeek Harness (dsh). Every line gets a unique 4-char hash (A-Za-z0-9) that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
5
5
  "keywords": [
6
6
  "anchors",