@oh-my-pi/pi-tui 16.3.5 → 16.3.7

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/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [16.3.7] - 2026-07-05
6
+
7
+ ### Fixed
8
+
9
+ - Fixed an issue where `@` file-reference tokens in slash-command arguments incorrectly triggered prompt-composer autocompletion when the command did not define argument completions.
10
+ - Fixed a memory leak caused by unbounded map growth in the image budget cache.
11
+
12
+ ## [16.3.6] - 2026-07-04
13
+
14
+ ### Added
15
+
16
+ - Added `Markdown.getLastRenderSettledRows()`: the rendered frozen-token-prefix row count of the most recent streaming render, exposed (hard-monotone per text lineage) for native-scrollback commit gating. Frozen-prefix code blocks now syntax-highlight during streaming renders so settled rows stay byte-stable across finalize.
17
+
18
+ ### Changed
19
+
20
+ - Rewrote the native-scrollback commit law around a visual-record model: whatever scrolls above the viewport enters history exactly once, in order — nothing painted ever vanishes. `NativeScrollbackLiveRegion` is now a single method (`getNativeScrollbackLiveRegionStart`) reporting an exactness boundary: rows below it commit as exact audited bytes; rows above it commit as frozen visual snapshots that are audit-exempt while their source stays live and strict-verified exactly once when the boundary passes them (a divergence recommits the final content below the frozen fragment — duplication, never loss). `getNativeScrollbackCommitSafeEnd`/`getNativeScrollbackSnapshotSafeEnd` and the heuristic promotion machinery they fed are removed.
21
+ - Simplified committed-prefix auditing to one hard-verified mark deriving three zones (verified/newly-final/frozen); a frame that shrinks below the committed row count re-bases at the first divergence against the recorded prefix so a collapsing live suffix never re-shows or re-commits already-recorded rows.
22
+
23
+ ### Fixed
24
+
25
+ - Fixed live tool/eval preview boxes spraying duplicated stale copies into native scrollback mid-run: a still-live block's scrolled rows are now frozen visual snapshots that never re-anchor while it runs; at most one repair happens at finalize.
26
+ - Fixed streamed content vanishing above the viewport mid-turn: scrolled-off rows always reach native scrollback (as exact bytes for declared-final content, as visual snapshots otherwise) instead of being deferred invisibly.
27
+
5
28
  ## [16.3.5] - 2026-07-04
6
29
 
7
30
  ### Fixed
@@ -19,6 +19,8 @@ export interface SlashCommand {
19
19
  aliases?: string[];
20
20
  description?: string;
21
21
  argumentHint?: string;
22
+ /** Whether the command consumes argument text after the command name. False means the full input stays normal prompt text once args are present. */
23
+ allowArgs?: boolean;
22
24
  /** Dynamic display-only description for slash-command autocomplete. Must be synchronous and side-effect free. */
23
25
  getAutocompleteDescription?: () => string | undefined;
24
26
  getArgumentCompletions?(argumentPrefix: string): Awaitable<AutocompleteItem[] | null>;
@@ -1,4 +1,4 @@
1
- import type { Component } from "../tui";
1
+ import type { Component } from "../tui.js";
2
2
  /** Box-drawing glyphs plus an optional colorizer for an outline drawn around a {@link Box}. */
3
3
  export interface BoxBorder {
4
4
  chars: {
@@ -1,4 +1,4 @@
1
- import { Loader } from "./loader";
1
+ import { Loader } from "./loader.js";
2
2
  /**
3
3
  * Loader that can be cancelled with Escape.
4
4
  * Extends Loader with an AbortSignal for cancelling async operations.
@@ -1,7 +1,7 @@
1
- import { type AutocompleteProvider } from "../autocomplete";
2
- import type { SymbolTheme } from "../symbols";
3
- import { type Component, type Focusable } from "../tui";
4
- import { type SelectListTheme } from "./select-list";
1
+ import { type AutocompleteProvider } from "../autocomplete.js";
2
+ import type { SymbolTheme } from "../symbols.js";
3
+ import { type Component, type Focusable } from "../tui.js";
4
+ import { type SelectListTheme } from "./select-list.js";
5
5
  export interface EditorTheme {
6
6
  borderColor: (str: string) => string;
7
7
  selectList: SelectListTheme;
@@ -1,5 +1,5 @@
1
- import { type ImageDimensions } from "../terminal-capabilities";
2
- import type { Component } from "../tui";
1
+ import { type ImageDimensions } from "../terminal-capabilities.js";
2
+ import type { Component } from "../tui.js";
3
3
  export interface ImageTheme {
4
4
  fallbackColor: (str: string) => string;
5
5
  }
@@ -1,4 +1,4 @@
1
- import { type Component, type Focusable } from "../tui";
1
+ import { type Component, type Focusable } from "../tui.js";
2
2
  /**
3
3
  * Input component - single-line text input with horizontal scrolling
4
4
  */
@@ -1,5 +1,5 @@
1
- import type { TUI } from "../tui";
2
- import { Text } from "./text";
1
+ import type { TUI } from "../tui.js";
2
+ import { Text } from "./text.js";
3
3
  type ColorFn = (str: string) => string;
4
4
  export type LoaderMessageColorFn = ColorFn & {
5
5
  readonly animated?: true;
@@ -1,5 +1,5 @@
1
- import type { SymbolTheme } from "../symbols";
2
- import type { Component } from "../tui";
1
+ import type { SymbolTheme } from "../symbols.js";
2
+ import type { Component } from "../tui.js";
3
3
  /** Drop all L2 cache entries. Call on theme change to prevent stale styled output. */
4
4
  export declare function clearRenderCache(): void;
5
5
  /**
@@ -55,6 +55,16 @@ export declare class Markdown implements Component {
55
55
  invalidate(): void;
56
56
  get transientRenderCache(): boolean;
57
57
  set transientRenderCache(value: boolean);
58
+ /**
59
+ * Rows at the top of the most recent render() (top padding + rendered
60
+ * frozen-token prefix) whose bytes are settled: byte-stable at this
61
+ * width/theme for as long as the text keeps growing append-only. Hosts
62
+ * feed this to transcript commit gating (see the coding agent's
63
+ * `FinalizableBlock.getTranscriptBlockSettledRows`). 0 outside streaming
64
+ * (`transientRenderCache`) mode, after a text rewind (re-earned on the new
65
+ * lineage), and on cache-served non-streaming renders.
66
+ */
67
+ getLastRenderSettledRows(): number;
58
68
  render(width: number): readonly string[];
59
69
  }
60
70
  /**
@@ -1,5 +1,5 @@
1
- import type { Component } from "../tui";
2
- import { Ellipsis } from "../utils";
1
+ import type { Component } from "../tui.js";
2
+ import { Ellipsis } from "../utils.js";
3
3
  type ScrollbarMode = "auto" | "always" | "never";
4
4
  export interface ScrollViewTheme {
5
5
  track?: (text: string) => string;
@@ -1,6 +1,6 @@
1
- import { type MouseRoutable, type SgrMouseEvent } from "../mouse";
2
- import type { SymbolTheme } from "../symbols";
3
- import type { Component } from "../tui";
1
+ import { type MouseRoutable, type SgrMouseEvent } from "../mouse.js";
2
+ import type { SymbolTheme } from "../symbols.js";
3
+ import type { Component } from "../tui.js";
4
4
  export interface SelectItem {
5
5
  value: string;
6
6
  label: string;
@@ -1,5 +1,5 @@
1
- import type { SgrMouseEvent } from "../mouse";
2
- import type { Component } from "../tui";
1
+ import type { SgrMouseEvent } from "../mouse.js";
2
+ import type { Component } from "../tui.js";
3
3
  export interface SettingItem {
4
4
  /** Unique identifier for this setting */
5
5
  id: string;
@@ -1,4 +1,4 @@
1
- import type { Component } from "../tui";
1
+ import type { Component } from "../tui.js";
2
2
  /**
3
3
  * Spacer component that renders empty lines
4
4
  */
@@ -1,4 +1,4 @@
1
- import type { Component } from "../tui";
1
+ import type { Component } from "../tui.js";
2
2
  /** Tab definition */
3
3
  export interface Tab {
4
4
  /** Unique identifier for the tab */
@@ -1,4 +1,4 @@
1
- import type { Component } from "../tui";
1
+ import type { Component } from "../tui.js";
2
2
  /**
3
3
  * Text component - displays multi-line text with word wrapping
4
4
  */
@@ -1,4 +1,4 @@
1
- import type { Component } from "../tui";
1
+ import type { Component } from "../tui.js";
2
2
  /**
3
3
  * Text component that truncates to fit viewport width
4
4
  */
@@ -1,4 +1,4 @@
1
- import type { TerminalId, TerminalNotification } from "./terminal-capabilities";
1
+ import type { TerminalId, TerminalNotification } from "./terminal-capabilities.js";
2
2
  /** Resolved notifier binary used to fan a notification out to D-Bus. */
3
3
  export type DesktopNotifierKind = "notify-send" | "gdbus";
4
4
  export interface DesktopNotifier {
@@ -1,5 +1,5 @@
1
- import type { AutocompleteProvider } from "./autocomplete";
2
- import type { Component } from "./tui";
1
+ import type { AutocompleteProvider } from "./autocomplete.js";
2
+ import type { Component } from "./tui.js";
3
3
  /**
4
4
  * Interface for custom editor components.
5
5
  *
@@ -1,32 +1,32 @@
1
- export * from "./autocomplete";
2
- export * from "./components/box";
3
- export * from "./components/cancellable-loader";
4
- export * from "./components/editor";
5
- export * from "./components/image";
6
- export * from "./components/input";
7
- export * from "./components/loader";
8
- export * from "./components/markdown";
9
- export * from "./components/scroll-view";
10
- export * from "./components/select-list";
11
- export * from "./components/settings-list";
12
- export * from "./components/spacer";
13
- export * from "./components/tab-bar";
14
- export * from "./components/text";
15
- export * from "./components/truncated-text";
16
- export * from "./deccara";
17
- export * from "./desktop-notify";
18
- export type * from "./editor-component";
19
- export * from "./fuzzy";
20
- export * from "./keybindings";
21
- export * from "./keys";
22
- export * from "./kitty-graphics";
23
- export * from "./latex-block";
24
- export * from "./latex-to-unicode";
25
- export * from "./mouse";
26
- export * from "./stdin-buffer";
27
- export type * from "./symbols";
28
- export * from "./terminal";
29
- export * from "./terminal-capabilities";
30
- export * from "./ttyid";
31
- export * from "./tui";
32
- export * from "./utils";
1
+ export * from "./autocomplete.js";
2
+ export * from "./components/box.js";
3
+ export * from "./components/cancellable-loader.js";
4
+ export * from "./components/editor.js";
5
+ export * from "./components/image.js";
6
+ export * from "./components/input.js";
7
+ export * from "./components/loader.js";
8
+ export * from "./components/markdown.js";
9
+ export * from "./components/scroll-view.js";
10
+ export * from "./components/select-list.js";
11
+ export * from "./components/settings-list.js";
12
+ export * from "./components/spacer.js";
13
+ export * from "./components/tab-bar.js";
14
+ export * from "./components/text.js";
15
+ export * from "./components/truncated-text.js";
16
+ export * from "./deccara.js";
17
+ export * from "./desktop-notify.js";
18
+ export type * from "./editor-component.js";
19
+ export * from "./fuzzy.js";
20
+ export * from "./keybindings.js";
21
+ export * from "./keys.js";
22
+ export * from "./kitty-graphics.js";
23
+ export * from "./latex-block.js";
24
+ export * from "./latex-to-unicode.js";
25
+ export * from "./mouse.js";
26
+ export * from "./stdin-buffer.js";
27
+ export type * from "./symbols.js";
28
+ export * from "./terminal.js";
29
+ export * from "./terminal-capabilities.js";
30
+ export * from "./ttyid.js";
31
+ export * from "./tui.js";
32
+ export * from "./utils.js";
@@ -1,4 +1,4 @@
1
- import { type KeyId } from "./keys";
1
+ import { type KeyId } from "./keys.js";
2
2
  /**
3
3
  * Global keybinding registry.
4
4
  * Downstream packages can add keybindings via declaration merging.
@@ -42,6 +42,12 @@ export declare class TerminalInfo {
42
42
  export declare function isInsideTmux(env?: NodeJS.ProcessEnv): boolean;
43
43
  /** Detect terminal multiplexers where scrollback clearing and height-change redraws are hostile. */
44
44
  export declare function isInsideTerminalMultiplexer(env?: NodeJS.ProcessEnv): boolean;
45
+ /**
46
+ * Whether the agent process is running inside a Zellij session. Read fresh on
47
+ * each call (like {@link isInsideTmux}) so a session attached/detached mid-run
48
+ * is observed and tests can toggle `Bun.env.ZELLIJ` per case.
49
+ */
50
+ export declare function isInsideZellij(env?: NodeJS.ProcessEnv): boolean;
45
51
  /**
46
52
  * Wrap a control-sequence payload in tmux's DCS passthrough envelope. Each
47
53
  * ESC byte inside `payload` is doubled per tmux's escape rules. tmux strips
@@ -1,4 +1,4 @@
1
- import { type HangulCompatibilityJamoWidth } from "./utils";
1
+ import { type HangulCompatibilityJamoWidth } from "./utils.js";
2
2
  export declare function resolveHangulCompatibilityJamoWidthFromTerminalIdentity(env?: NodeJS.ProcessEnv): HangulCompatibilityJamoWidth;
3
3
  /**
4
4
  * Split `data` into chunks whose encoded UTF-8 byte length is no greater than
@@ -1,6 +1,6 @@
1
- import { ImageBudget } from "./components/image";
2
- import { type Terminal } from "./terminal";
3
- import { visibleWidth } from "./utils";
1
+ import { ImageBudget } from "./components/image.js";
2
+ import { type Terminal } from "./terminal.js";
3
+ import { visibleWidth } from "./utils.js";
4
4
  type InputListenerResult = {
5
5
  consume?: boolean;
6
6
  data?: string;
@@ -76,45 +76,23 @@ export interface OverlayFocusOwner {
76
76
  ownsOverlayFocusTarget(component: Component): boolean;
77
77
  }
78
78
  /**
79
- * Component seam for append-only native-scrollback commits. A component that
80
- * renders a finalized prefix followed by a live/mutating suffix reports the
81
- * local line index where that suffix begins after each render. The engine
82
- * commits rows to native scrollback only up to that boundary; everything
83
- * below repaints in place inside the visible window and never enters history
84
- * until it finalizes.
79
+ * Component seam for append-only native-scrollback commits. A component whose
80
+ * rendered rows can still change reports, after each render, the local line
81
+ * index where that mutable suffix begins. Rows above the boundary are declared
82
+ * FINAL byte-stable at the current width for the component's lifetime — and
83
+ * commit to native scrollback as exact, audited content. Rows at/after the
84
+ * boundary repaint in place inside the visible window; when they scroll above
85
+ * the window top they still commit — the tape records what was on screen —
86
+ * but as frozen visual snapshots that are permanently audit-exempt: later
87
+ * re-layout of their source never re-anchors or recommits them. A root that
88
+ * reports no seam commits everything that scrolls as final (shell semantics).
85
89
  *
86
- * `getNativeScrollbackCommitSafeEnd` optionally reports a *deeper* boundary
87
- * inside the live suffix: the line index up to which the live region is
88
- * append-only (earlier rows never re-layout — a streaming assistant message).
89
- * Rows in `[liveRegionStart, commitSafeEnd)` may commit even though they are
90
- * technically live, because they will never change. Without it, a single live
91
- * block that alone overflows the window would hold its scrolled-off head out
92
- * of history until it finalizes. Volatile live blocks (tool previews that
93
- * collapse) omit it. Defaults to `liveRegionStart` when absent; a root that
94
- * reports no seam at all commits everything that scrolls (shell semantics).
95
- * `getNativeScrollbackSnapshotSafeEnd` optionally reports a still deeper
96
- * boundary: the line index up to which the live region is *durable* — its rows
97
- * may still change bytes later (a streaming markdown table re-aligning its
98
- * columns every row), but their CURRENT snapshot is permanent content, so
99
- * dropping them when they scroll above the window is forbidden. Unlike
100
- * `commitSafeEnd` (byte-stable: offered rows are asserted never to re-layout and
101
- * stay under the committed-prefix audit), rows committed under the snapshot end
102
- * are audit-EXEMPT once they pass the window top — the engine appends their
103
- * scroll-off snapshot and never recommits them, so later layout drift becomes a
104
- * frozen stale row in history (duplication never loss) instead of either a
105
- * dropped row or an audit re-anchor spray. Provisional live blocks (collapsing
106
- * tool/edit previews whose head is a throwaway tail window) omit it. Defaults to
107
- * `commitSafeEnd ?? liveRegionStart` when absent.
108
- *
109
- * When several root children report a seam in the same frame, the topmost
110
- * one (and its commit-safe / snapshot-safe extension) defines the boundary:
111
- * commits are prefix-only, so everything below the first seam is already
112
- * excluded.
90
+ * When several root children report a seam in the same frame, the topmost one
91
+ * defines the boundary: exactness is prefix-only, so everything below the
92
+ * first seam is already excluded.
113
93
  */
114
94
  export interface NativeScrollbackLiveRegion {
115
95
  getNativeScrollbackLiveRegionStart(): number | undefined;
116
- getNativeScrollbackCommitSafeEnd?(): number | undefined;
117
- getNativeScrollbackSnapshotSafeEnd?(): number | undefined;
118
96
  }
119
97
  export interface NativeScrollbackCommittedRows {
120
98
  setNativeScrollbackCommittedRows(rows: number): void;
@@ -294,40 +272,34 @@ export declare function coalesceAdjacentSgr(line: string): string;
294
272
  * re-anchor the commit index when it does not. Returns the resync row index,
295
273
  * or -1 when no resync is needed.
296
274
  *
297
- * Audits the committed prefix [0, auditTo) EXCEPT the exempt window
298
- * [exemptFrom, exemptTo): rows in the window are durable snapshots (a streaming
299
- * table re-aligning its columns) that may drift legitimately, so their drift
300
- * never triggers a re-anchor. Rows below the window including forced-overflow
301
- * rows committed only because they scrolled above the viewport under a
302
- * commit-unstable barrier ARE audited.
303
- *
304
- * Two detectors run over the audited rows:
275
+ * Zones (verifiedTo finalTo prefix.length):
276
+ * [0, verifiedTo) VERIFIED exact rows sampled with tolerance.
277
+ * [verifiedTo, finalTo) NEWLY-FINAL rows frozen visual snapshots whose
278
+ * source just became declared-final (the block finalized / a barrier
279
+ * cleared). Hard-scanned in FULL with no tolerance: any content change
280
+ * (a pending header settling, a preview replaced by its result, a tail
281
+ * shifting up after a barrier removal) re-anchors so the final content
282
+ * recommits below the frozen snapshot duplication, never loss —
283
+ * instead of being committed nowhere and painted nowhere.
284
+ * [finalTo, prefix.length) FROZEN visual snapshots of still-live rows —
285
+ * exempt: their drift is expected (a collapsing preview, a ticking
286
+ * progress tree) and must never spray re-anchors mid-run.
305
287
  *
306
- * 1. Hard scan of the now-permanent forced suffix [exemptTo, permanentEnd):
307
- * forced-overflow rows that THIS frame asserts are durable/permanent (index <
308
- * permanentEnd the barrier above them finalized or cleared, so durableBoundary
309
- * rose past them). A content change there is real finalized content, so ANY
310
- * mismatch re-anchors. Scanned in FULL, not sampled, so a single edit far above
311
- * the commit boundary with an unchanged tail still re-anchors (duplication,
312
- * never loss) instead of being committed nowhere and painted nowhere.
313
- * 2. Tail sample (only when the hard scan is clean): exploits the asymmetry
314
- * between the two mutation classes — an in-place edit/restyle of a committed
315
- * row disturbs only the touched rows (alignment below intact; the stale copy
316
- * in history is the long-accepted artifact), while an insertion/deletion
317
- * shifts EVERY row below it. So up to 8 non-blank rows within the last 24
318
- * audited rows are compared SGR-stripped (theme changes stay quiet),
319
- * tolerating a SINGLE non-hard mismatch (a legitimate one-row edit): aligned ⇒
320
- * no resync; misaligned ⇒ resync at the first non-equivalent audited row. The
321
- * tolerance keeps both an offscreen still-live barrier (a ticking spinner) and
322
- * a no-seam in-place row edit from spraying duplicate snapshots every frame;
323
- * the hard scan above is what forbids it from swallowing a finalized row.
288
+ * The verified zone's sampled check exploits the asymmetry between the two
289
+ * mutation classes: an in-place edit/restyle disturbs only the touched rows
290
+ * (alignment below stays intact; the stale copy in history is the accepted
291
+ * artifact), while an insertion/deletion shifts EVERY row below it. Up to 8
292
+ * non-blank rows within the last 24 verified rows are compared SGR-stripped
293
+ * (theme changes stay quiet), tolerating a SINGLE mismatch. The tolerance is
294
+ * load-bearing for roots that report NO seam: an animated row already in
295
+ * history would otherwise re-anchor on every glyph tick.
324
296
  *
325
297
  * Highly repetitive tails (identical filler rows) can mask a shift in the tail
326
298
  * sample, in which case the skipped rows are content-identical to the committed
327
299
  * ones — observationally harmless. Exported for the render-stress harness, whose
328
300
  * shadow commit ledger must mirror the engine's law exactly.
329
301
  */
330
- export declare function findCommittedPrefixResync(frame: readonly string[], prefix: readonly string[], auditTo?: number, exemptFrom?: number, exemptTo?: number, permanentEnd?: number): number;
302
+ export declare function findCommittedPrefixResync(frame: readonly string[], prefix: readonly string[], verifiedTo?: number, finalTo?: number): number;
331
303
  /**
332
304
  * TUI - Main class for managing terminal UI with differential rendering
333
305
  */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-tui",
4
- "version": "16.3.5",
4
+ "version": "16.3.7",
5
5
  "description": "Terminal User Interface library with differential rendering for efficient text-based applications",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -37,8 +37,8 @@
37
37
  "fmt": "biome format --write ."
38
38
  },
39
39
  "dependencies": {
40
- "@oh-my-pi/pi-natives": "16.3.5",
41
- "@oh-my-pi/pi-utils": "16.3.5",
40
+ "@oh-my-pi/pi-natives": "16.3.7",
41
+ "@oh-my-pi/pi-utils": "16.3.7",
42
42
  "lru-cache": "11.5.1",
43
43
  "marked": "^18.0.5"
44
44
  },
@@ -182,6 +182,8 @@ export interface SlashCommand {
182
182
  aliases?: string[];
183
183
  description?: string;
184
184
  argumentHint?: string;
185
+ /** Whether the command consumes argument text after the command name. False means the full input stays normal prompt text once args are present. */
186
+ allowArgs?: boolean;
185
187
  /** Dynamic display-only description for slash-command autocomplete. Must be synchronous and side-effect free. */
186
188
  getAutocompleteDescription?: () => string | undefined;
187
189
  // Function to get argument completions for this command
@@ -387,44 +389,18 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
387
389
  const currentLine = lines[cursorLine] || "";
388
390
  const textBeforeCursor = currentLine.slice(0, cursorCol);
389
391
 
390
- // Check for @ file reference (fuzzy search) - must be after a delimiter or at start
391
- const atPrefix = this.#extractAtPrefix(textBeforeCursor);
392
- if (atPrefix) {
393
- const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
394
- // Recursive fuzzy walks rooted outside the project (e.g. `@../`,
395
- // `@~/`, `@/abs`) can be huge — a parent dir full of sibling
396
- // projects blows past several seconds of latency. Outside cwd,
397
- // fall back to plain prefix listing of the immediate directory
398
- // (matches Claude Code's behavior). Inside cwd we keep the
399
- // fuzzy-then-prefix flow.
400
- if (rawPrefix.length > 0 && this.#isOutsideCwd(rawPrefix)) {
401
- const items = await this.#getFileSuggestions(atPrefix);
402
- if (items.length === 0) return null;
403
- return { items, prefix: atPrefix };
404
- }
405
- const suggestions =
406
- rawPrefix.length > 0
407
- ? await this.#getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix })
408
- : await this.#getFileSuggestions("@");
409
- if (suggestions.length === 0 && rawPrefix.length > 0) {
410
- const fallback = await this.#getFileSuggestions(atPrefix);
411
- if (fallback.length === 0) return null;
412
- return { items: fallback, prefix: atPrefix };
413
- }
414
- if (suggestions.length === 0) return null;
415
-
416
- return {
417
- items: suggestions,
418
- prefix: atPrefix,
419
- };
420
- }
421
-
422
392
  const leadingSlashStart = findLeadingSlashCommandStart(textBeforeCursor);
423
393
  const trailingSlashStart = findTrailingSlashCommandStart(textBeforeCursor);
424
394
  const hasPromptTextBeforeTrailingSlash =
425
395
  trailingSlashStart !== null &&
426
396
  hasPromptTextBeforeSlash(lines, cursorLine, textBeforeCursor, trailingSlashStart);
427
- const slashStart = hasPromptTextBeforeTrailingSlash ? trailingSlashStart : leadingSlashStart;
397
+ const hasPromptTextBeforeLeadingSlash =
398
+ leadingSlashStart !== null && hasPromptTextBeforeSlash(lines, cursorLine, textBeforeCursor, leadingSlashStart);
399
+ const slashStart = hasPromptTextBeforeTrailingSlash
400
+ ? trailingSlashStart
401
+ : hasPromptTextBeforeLeadingSlash
402
+ ? null
403
+ : leadingSlashStart;
428
404
  if (slashStart !== null) {
429
405
  const commandText = textBeforeCursor.slice(slashStart);
430
406
  const spaceIndex = commandText.indexOf(" ");
@@ -453,26 +429,62 @@ export class CombinedAutocompleteProvider implements AutocompleteProvider {
453
429
  // A mid-prompt slash token with no matching skill may still be an
454
430
  // absolute path (`see /tmp`); fall through to file-path completion.
455
431
  } else if (!isMidPromptSkillLookup) {
456
- // Space found - complete command arguments
432
+ // Submitted slash commands own their argument text only when the
433
+ // matched command accepts args. No-arg slash-looking prompts such
434
+ // as `/settings @file` still fall through to prompt-composer
435
+ // completions because submit treats them as normal prompt text.
457
436
  const commandName = commandText.slice(1, spaceIndex); // Command without "/"
458
437
  const argumentText = commandText.slice(spaceIndex + 1); // Text after space
459
438
 
460
439
  const command = this.#commands.find(cmd => commandMatchesNameOrAlias(cmd, commandName));
461
- if (!command || !("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
462
- return null; // No argument completion for this command
463
- }
440
+ if (command && (!("allowArgs" in command) || command.allowArgs !== false)) {
441
+ if (!("getArgumentCompletions" in command) || !command.getArgumentCompletions) {
442
+ return null; // No argument completion for this command
443
+ }
444
+
445
+ const argumentSuggestions = await command.getArgumentCompletions(argumentText);
446
+ if (!Array.isArray(argumentSuggestions) || argumentSuggestions.length === 0) {
447
+ return null;
448
+ }
464
449
 
465
- const argumentSuggestions = await command.getArgumentCompletions(argumentText);
466
- if (!Array.isArray(argumentSuggestions) || argumentSuggestions.length === 0) {
467
- return null;
450
+ return {
451
+ items: argumentSuggestions,
452
+ prefix: argumentText,
453
+ };
468
454
  }
455
+ }
456
+ }
469
457
 
470
- return {
471
- items: argumentSuggestions,
472
- prefix: argumentText,
473
- };
458
+ // Check for @ file reference (fuzzy search) - must be after a delimiter or at start
459
+ const atPrefix = this.#extractAtPrefix(textBeforeCursor);
460
+ if (atPrefix) {
461
+ const { rawPrefix, isQuotedPrefix } = parsePathPrefix(atPrefix);
462
+ // Recursive fuzzy walks rooted outside the project (e.g. `@../`,
463
+ // `@~/`, `@/abs`) can be huge — a parent dir full of sibling
464
+ // projects blows past several seconds of latency. Outside cwd,
465
+ // fall back to plain prefix listing of the immediate directory
466
+ // (matches Claude Code's behavior). Inside cwd we keep the
467
+ // fuzzy-then-prefix flow.
468
+ if (rawPrefix.length > 0 && this.#isOutsideCwd(rawPrefix)) {
469
+ const items = await this.#getFileSuggestions(atPrefix);
470
+ if (items.length === 0) return null;
471
+ return { items, prefix: atPrefix };
474
472
  }
475
- if (!isMidPromptSkillLookup) return null;
473
+ const suggestions =
474
+ rawPrefix.length > 0
475
+ ? await this.#getFuzzyFileSuggestions(rawPrefix, { isQuotedPrefix })
476
+ : await this.#getFileSuggestions("@");
477
+ if (suggestions.length === 0 && rawPrefix.length > 0) {
478
+ const fallback = await this.#getFileSuggestions(atPrefix);
479
+ if (fallback.length === 0) return null;
480
+ return { items: fallback, prefix: atPrefix };
481
+ }
482
+ if (suggestions.length === 0) return null;
483
+
484
+ return {
485
+ items: suggestions,
486
+ prefix: atPrefix,
487
+ };
476
488
  }
477
489
 
478
490
  // Check for file paths - triggered by Tab or if we detect a path pattern