diff --git a/docs/design/0161-file-explorer-search-narrow.svg b/docs/design/0161-file-explorer-search-narrow.svg
new file mode 100644
index 00000000..9df51b1f
--- /dev/null
+++ b/docs/design/0161-file-explorer-search-narrow.svg
@@ -0,0 +1,16 @@
+
diff --git a/docs/design/0161-file-explorer-search-wide.svg b/docs/design/0161-file-explorer-search-wide.svg
new file mode 100644
index 00000000..4b471cfb
--- /dev/null
+++ b/docs/design/0161-file-explorer-search-wide.svg
@@ -0,0 +1,16 @@
+
diff --git a/docs/design/0161-file-explorer-search.md b/docs/design/0161-file-explorer-search.md
new file mode 100644
index 00000000..411ebcc3
--- /dev/null
+++ b/docs/design/0161-file-explorer-search.md
@@ -0,0 +1,314 @@
+---
+title: "DX-0161 - Sticky explorer search"
+legend: "DX"
+lane: "design"
+issue: "https://github.com/flyingrobots/jedit/issues/340"
+status: "active"
+owners:
+ - "@flyingrobots"
+created: "2026-10-14"
+updated: "2026-10-14"
+---
+
+# DX-0161 - Sticky explorer search
+
+## Linked Issue
+
+- https://github.com/flyingrobots/jedit/issues/340
+
+## Decision Summary
+
+Give the Files drawer modal navigation, a sticky editable search field, pointer
+hover/selection, read-only file previews, and live fuzzy filtering
+of the directory entries already loaded by the workspace. The query is a
+session-only UI projection; there is no persistent index, SQLite, recursive
+scan, semantic retrieval, or symbol extraction in this cycle.
+
+## Sponsored Human
+
+A keyboard user opens Files and wants to immediately type a partial file name
+or relative path, see matching entries, and open one without switching modes.
+
+## Sponsored Agent
+
+An agent needs inspectable query/cursor state and one shared filtered-entry
+projection for painting, navigation, opening, and pointer hit-testing.
+
+## Hill
+
+Ctrl+B opens navigation; / enters the pinned filter. j/k, arrows, and clicks
+select results and show read-only previews without replacing the active buffer.
+Enter opens exactly the selected match through the existing file-open path.
+
+## Current Truth
+
+The Files drawer renders only a scrollable list. Its focused keys perform
+navigation and directory actions; there is no input or query state. See
+[viewer-drawers.ts#14:5d7b0957a2511ac28b9ec9f47c9e6c432f89deea](https://github.com/flyingrobots/jedit/blob/5d7b0957a2511ac28b9ec9f47c9e6c432f89deea/src/app/workspace/viewer-drawers.ts#L14)
+and [file-tree.ts#47:5d7b0957a2511ac28b9ec9f47c9e6c432f89deea](https://github.com/flyingrobots/jedit/blob/5d7b0957a2511ac28b9ec9f47c9e6c432f89deea/src/app/workspace/file-tree.ts#L47).
+
+Graft exposes per-file outline projections through `GraftSessionPort`, not a
+ready-made workspace-wide symbol inventory. This PR is stacked on #339.
+
+## Problem
+
+Opening the explorer does not provide a place to type. Printable keys currently
+perform actions such as navigation, refresh, quit, or perf toggling.
+
+## Scope
+
+- Pinned single-line input, automatic focus, visible caret, horizontal query scrolling.
+- Case-insensitive ordered-subsequence matching of loaded names and relative paths.
+- Shared filtered-entry identity for render, keys, wheel, click, and file opening.
+- Query editing, empty results, directory transition, and focus/close behavior.
+
+## Non-Goals
+
+- Recursive workspace inventory, semantic retrieval, Graft symbol scan, SQLite, persistence.
+- Leader configuration/dispatch or changes to authoritative Jim/Echo text state.
+
+## User Experience / Product Shape
+
+The refinement replaces automatic input editing with explicit explorer modality.
+Ctrl+B opens Files in navigation mode. j/k and Up/Down select results; `/` enters
+filter editing. Clicking the input also enters editing. Esc or Enter in editing
+returns to navigation while preserving the query. Esc in navigation closes the
+drawer. Ctrl+B closes from either mode. Reopening starts in navigation with the
+session query retained. F2 and Tab leave filter editing before changing focus.
+
+The search field remains pinned and uses a distinct theme background. Its caret
+is visible only while editing and Files owns focus. Query edits select the first
+match. Matched filename characters retain the accent and bold styling. Directory
+changes clear the query. j/k/r/Backspace/h/l regain list actions in navigation;
+Ctrl+R refreshes and Alt+Up goes to the parent in either mode.
+
+Pointer motion highlights the row under the cursor without changing selection
+or loading a preview. Hover and selection have distinct theme backgrounds;
+selection takes precedence if the cursor is over the selected row. Clicking a
+row selects it and returns to navigation. A file selection previews it in the
+main viewer while focus remains on Files. Clicking a directory only selects it;
+Enter enters it. Enter on a file opens it through the native Echo path and moves
+focus to the editor. Closing or leaving Files restores the existing editor.
+
+Preview is an explicitly labelled, read-only, bounded projection. The current
+active buffer and other open buffer projections are preferred so unsaved text is
+visible. Otherwise a filesystem adapter reads at most 64 KiB and 200 lines.
+Binary, directory, unavailable, loading, and truncated previews are labelled.
+Preview does not create an Echo buffer, change text authority, edit, or save.
+Request identity and selected source entry reject late results. Selecting a
+new item replaces the previous preview with loading immediately; no matches or
+non-file selection clears it. No persistent preview cache is introduced.
+
+Terminal input must deliver Unicode scalars and modified arrows through a
+Jedit boundary adapter; all-motion mouse reporting enables hover. Windows path
+normalization occurs at the filesystem boundary with a portable search-path
+field, leaving native paths intact for opening. Existing review findings on
+these boundaries are included in this refinement.
+
+### User Journey
+
+Open Files → select with j/k or pointer → inspect preview → Enter opens.
+Press / → type query → Esc → choose a filtered result. Backspace
+or Ctrl+U restores broader results; Esc closes and returns to the normal pane.
+
+### Wide UI Mockup
+
+100×32 workspace, existing drawer/theme. The input row and separator stay pinned.
+
+
+
+### Narrow UI Mockup
+
+60×16 workspace. The query scrolls horizontally; the selected row remains visible.
+
+
+
+### Accessibility Considerations
+
+Text label, visible caret, visible selected result, and an explicit no-match
+message. Keyboard-only editing and opening; focus is never inferred from color
+alone. Pointer mapping uses the same geometry as rendering. Existing minimum
+terminal geometry remains unchanged.
+
+## Runtime / API Contract
+
+`WorkspaceModel.fileSearch` holds input and cursor coordinates;
+`fileSearchEditing` declares filter mode and `fileHoveredIndex` is independent
+of selection. `fileExplorerPreview` carries a selected entry and request id. A pure selector
+returns visible entries with their source indexes; `selectedIndex` retains the
+loaded-entry index so existing file-opening identity is preserved. Query changes
+select the first match or no selection. All filtered actions use that selector.
+The input handler runs before plain global shortcuts only in filter mode;
+Ctrl+C, close, pane switching, and Settings retain their existing routes.
+
+## Lower Modes
+
+Input, query, selected rows, and no-match text remain visible without color.
+The caret uses the existing theme token. Small viewports clip safely. No Graft
+or optional database adapter is required; unreadable directory errors retain
+existing file-system feedback and preserve the prior query/list.
+
+## Data / State Model
+
+| Category | Description |
+| --- | --- |
+| Source of truth | Loaded FileEntry list, session mode/query/caret, selected entry, and bounded preview observation. |
+| Derived state | Filtered entries, selection position, text window, list scroll offset. |
+| Invalid states | Controls in query; opening an entry excluded by the current query. |
+| Reset behavior | Successful directory change clears query; closing preserves it. |
+| Persistence | None. A restart begins with an empty query and navigation mode. |
+| Causal boundary | Disposable presentation over loaded file-system entries; no authoritative index or text mutation. |
+
+## Accessibility Posture
+
+Input focus stays on Files during arrow selection. Opening a file returns focus
+to the existing editor/open path. Closing uses the existing pane restoration.
+Empty results are readable rather than silent. Search is scoped to the loaded
+directory, so it does not claim a complete workspace inventory. Hover has a
+quiet background; selection has an accent background with contrast-adjusted
+text. Existing theme variants keep them distinct.
+
+## Localization / Directionality Posture
+
+New search label, placeholder, empty-results text, and footer hints use catalog
+keys for every authored locale and the generated mirror locale. File paths keep
+their literal order; existing drawer directionality remains. Width calculations
+use terminal display-cell lengths, and Unicode input boundaries are preserved.
+
+## Agent Inspectability / Explainability Posture
+
+Inspect query/caret, selected source index, and the visible-entry selector.
+Runtime tests assert the requested path rather than a fabricated open result.
+Render tests inspect fixed row positions and cursor tokens. Live PTY exercises
+real input decoding and the native-host file-open route.
+
+## Linked Invariants
+
+- Source lists and buffers are not optimistically mutated by filtering.
+- Derived UI indexes do not become causal or filesystem authority.
+- One list projection and geometry serve all result interactions.
+- Tests prove real runtime behavior and rendering.
+
+## Design Alternatives Considered
+
+An input-only field would not help locate files. A small deterministic fuzzy
+filter requires no index or additional service. A repository crawler or symbol
+scan needs bounded asynchronous inventory and stale-result rules, so it is
+tracked separately in #341. SQLite has no demonstrated need in this slice.
+
+## Decision
+
+Ship the pinned input and ephemeral fuzzy filtering over loaded entries. Keep
+workspace-wide and symbol search separate.
+
+## Implementation Slices
+
+- [x] Slice 1: Input state, fuzzy projection, and keyboard/open behavior.
+- [x] Slice 2: Sticky input/caret rendering, pointer/wheel mapping, and localized help.
+- [x] Slice 3: Runtime/render/native-host verification and documentation.
+- [x] Slice 4: Explicit navigation/filter modes and mode-aware help.
+- [x] Slice 5: Pointer hover, click selection, and distinct row colors.
+- [x] Slice 6: Bounded read-only viewer previews and stale-result rejection.
+- [x] Slice 7: Real input boundary fixes, native verification, and refined docs.
+
+## Tests To Write First
+
+- [x] Ctrl+B focuses a visible input; printable shortcut keys type query text.
+- [x] Fuzzy matching, no matches, editing, and Unicode caret movement behave correctly.
+- [x] Arrows/Enter/click open the displayed match rather than a hidden source entry.
+- [x] Header stays pinned while results scroll; mouse/wheel use the filtered list.
+- [x] Close/reopen, Settings, pane switching, refresh, and directory transitions preserve the contract.
+
+## Acceptance Criteria
+
+- [x] Input is pinned, focused, and visibly editable on open.
+- [x] Loaded paths filter live with no persistent cache or index.
+- [x] All result interactions share the same projection and geometry.
+- [x] Empty results cannot open a hidden file.
+- [x] Full repository checks, localized rendering, and live PTY validation pass.
+
+## Validation Plan
+
+```bash
+npm run check
+npm run ci:verify-shards
+```
+
+## Playback / Witness
+
+Ctrl+B → / → type partial name → Esc → j/k or click → inspect preview → Enter. Repeat with no matches, clear with
+Ctrl+U, edit in the middle, scroll a long list, and click a filtered row. Inspect
+at 100×32 and 60×16, including the real native-host file-open path.
+
+## Risks
+
+Existing j/k/r/Backspace navigation is preserved in navigation and becomes
+text editing only in filter mode; footer help must match. Filtering changes displayed indexes, so stale
+raw-index pointer calculations can open the wrong file. Unknown-symbol and
+workspace-inventory claims must remain outside this feature.
+
+## Follow-On Debt
+
+[Issue #341](https://github.com/flyingrobots/jedit/issues/341) owns evaluation of
+workspace-wide file, semantic, and Graft symbol search with disposable indexes.
+
+## Retrospective
+
+Implemented with one shared filtered projection retaining original entry indexes.
+The input owns printable keys while focus remains on Files; pointer geometry
+reserves the same pinned rows as painting. Match highlights use the filter's
+Unicode character positions, preferring filename matches when possible.
+
+The initial red tests exposed the missing input state and later the missing
+visual distinction. Existing list-navigation and pointer tests were updated for
+arrow navigation and the two extra pinned rows. Thirteen focused runtime/render
+tests pass, including Japanese, Arabic, and mirrored labels, wide-character
+caret mapping, and distinct input backgrounds across 16 built-in theme variants.
+The full repository check passes, and shard coverage includes all 131 specs.
+
+Live native-host PTY sessions at 100×32 and 60×16 confirmed decoded input,
+focused query editing, no-match Enter safety, and the selected fixture's actual
+Echo import result with its expected source content. Terminal snapshots were
+inspected at both sizes; the input remained at the top and matched characters
+were accented. Quality reports no regressions and its debt baseline is unchanged.
+
+No persistent query, recursive inventory, symbols, semantic retrieval, or cache
+was added. These remain explicit follow-on work in #341. This change is stacked
+on the static-title removal in #339; neither PR is merged by this cycle.
+
+### Refinement acceptance (2026-10-14)
+
+- [x] j/k navigates after opening; slash/click edits; Esc returns to navigation.
+- [x] Hover never selects or reads a file; click selects; colors remain distinct.
+- [x] Selected files preview without changing buffers, dirty text, or authority.
+- [x] Bounded, unavailable, binary, stale-result, and focus-return cases pass.
+- [x] Actual terminal Unicode, modified arrows, and hover packets are exercised.
+- [x] Full local checks and CI pass; documentation reflects final behavior.
+
+Refinement retrospective: implemented in `3a940469`, replacing always-editing
+focus with navigation/filter modes. Mouse click now selects, and hover stays
+independent. Selection shows a bounded, read-only preview; Enter remains the
+explicit native Echo open boundary. The previous active buffer is preserved.
+Preview results carry original entry identity and a request id, rejecting stale
+selection and closed-drawer results. Existing open buffers provide live text.
+
+The focused tests prove navigation and filter transitions, filtered pointer
+geometry, independent hover, preview rendering, stale-result rejection, unsaved
+text preservation, bounded UTF-8 reads, binary/unavailable handling, and a
+nonblocking named-pipe case. Event-bus tests exercise the production input
+adapter with Unicode, modified arrows, split CSI, lone Escape, and SGR hover.
+Portable paths are normalized at the filesystem boundary. Selection/hover stay
+distinct across 16 theme variants, with selected text checked for contrast.
+
+The full local check passes: 855 JavaScript tests, 12 existing skips, and all 99
+native tests. Quality has no regressions and shard coverage includes 134 specs.
+Live 100×32 and 60×16 PTYs confirm j/k navigation, hover without selection or
+preview reads, click selection/preview, labelled main-viewer content, real
+Unicode filtering/opening, and modified Alt+Up input. Terminal snapshots were
+visually inspected at both sizes. The narrow witness opened the selected file
+through Echo only after Enter.
+
+No persistent index/cache, workspace crawl, semantic retrieval, or Graft symbol
+scan was added; #341 remains the follow-on. No buffer authority was delegated
+to the preview. PR #342 remains stacked on #339 and requires separate merge
+authorization. CI completion is recorded in the PR.
diff --git a/docs/jim-component-ownership.md b/docs/jim-component-ownership.md
index 63c26c7e..c4e32f74 100644
--- a/docs/jim-component-ownership.md
+++ b/docs/jim-component-ownership.md
@@ -285,6 +285,20 @@ the meaning of a checkpoint declaration, while Echo owns the Tick and receipt
that witness its persistence. Jedit owns a screen cache, while neither that
cache nor the screen becomes text authority.
+The current Files drawer search and read-only preview are transient Jedit
+presentation projections. Navigation/filter mode, query/caret, hover, and
+selection are session-only shell state; they are not persisted or replayed as
+Jim buffer-search state. The filter creates no database or authoritative
+inventory. Selection previews bounded filesystem content or an already open
+buffer projection without replacing the active buffer, editing, saving, or
+creating an Echo buffer. Entry identity and request identity reject stale
+preview results. Native file paths remain intact while portable search paths
+are normalized at the filesystem boundary. Opening explicitly with Enter
+continues through the native Echo text-import path.
+[DX-0161](design/0161-file-explorer-search.md) owns this bounded implementation;
+a future workspace or symbol index must remain a rebuildable projection and
+declare its observation basis and staleness policy.
+
> **Intuition to carry forward:** semantic ownership and storage authority are
> different powers. Jim decides what command to perform, the text capability
> defines what the text operation means, and Echo decides whether their
diff --git a/docs/topics/ui/drawers-and-panels.md b/docs/topics/ui/drawers-and-panels.md
index 671f4444..a9b8029d 100644
--- a/docs/topics/ui/drawers-and-panels.md
+++ b/docs/topics/ui/drawers-and-panels.md
@@ -23,11 +23,57 @@ enough for detailed explanations.
## Files Drawer
-Open the files drawer with `ctrl+b`.
+Open the files drawer with `Ctrl+B`. It starts in navigation mode. The sticky
+search field has a distinct background, but takes keyboard input only after `/`
+or a click on the field. Its caret appears while editing.
-The files drawer is for browsing workspace entries. It should share file-opening
-behavior with `:edit`: selecting a file opens it, and selecting a directory
-enters that directory.
+| Navigation key | Action |
+| --- | --- |
+| `j` / `k`, `Up` / `Down` | Select a displayed result and preview a file. |
+| `/` | Edit the filter. |
+| `Enter` / `l` | Open the selected file for editing, or enter a directory. |
+| `Backspace` / `h`, `Alt+Up` | Enter the parent directory. |
+| `r` / `Ctrl+R` | Refresh the current directory. |
+| `Ctrl+B` / `Esc` | Close the drawer. |
+| `Tab` | Cycle visible panes. |
+
+| Filter key | Action |
+| --- | --- |
+| Printable characters | Fuzzy-filter loaded entries as you type. |
+| `Left` / `Right`, `Home` / `End` | Move the query caret. |
+| `Backspace` / `Delete` | Edit the query. |
+| `Ctrl+U` | Clear the query. |
+| `Esc` / `Enter` | Return to navigation and retain the query. |
+| `Up` / `Down` | Select a displayed result. |
+| `Ctrl+B` | Close the drawer. |
+
+Matching is case-insensitive and accepts characters in order, so `rnt` can match
+`render-tree.ts`. It preserves the directory's existing order. Matched filename
+characters use accent color and bold weight; the selected row keeps a readable
+foreground against its accent background. Empty results cannot open a hidden
+file. Closing and reopening preserves the session query and starts in navigation.
+Entering another directory clears it; refreshing preserves it. A restart starts
+with an empty query.
+
+Mouse motion highlights the row under the cursor without selecting it or reading
+a preview. Hover and selection have distinct backgrounds. A click selects the
+row and leaves focus in Files. Selecting a file, by keyboard, click, wheel, or
+filter change, shows a labelled read-only preview in the main viewer. Enter opens
+it through the native Echo text-import path and focuses the editor. A directory
+click selects it; Enter enters it.
+
+Previewing preserves the active buffer, including unsaved edits. Existing buffer
+projections supply their current text; other files are read at most 64 KiB and
+200 lines. Binary, unavailable, loading, and truncated previews are labelled.
+Late results cannot replace the current selection's preview. Closing or leaving
+Files restores the existing editor. Previews create no Echo buffers and have no
+save action or persistent cache. The projection boundary is recorded in
+[component ownership](../../jim-component-ownership.md).
+
+Search covers currently loaded directory entries. Workspace-wide discovery,
+semantic retrieval, and Graft symbols are deferred to
+[issue #341](https://github.com/flyingrobots/jedit/issues/341). No SQLite database
+or persistent index is created.
## Graft Drawer
@@ -67,4 +113,6 @@ replaces the explanation.
| [`src/ui/graft-drawer.ts`](../../../src/ui/graft-drawer.ts) | Graft drawer rendering. |
| [`src/ui/graft-diagnostics-panel.ts`](../../../src/ui/graft-diagnostics-panel.ts) | Diagnostics panel rendering. |
| [`src/ui/why-inline-panel.ts`](../../../src/ui/why-inline-panel.ts) | Inline why explanation rendering. |
+| [`src/app/workspace/file-explorer-preview.ts`](../../../src/app/workspace/file-explorer-preview.ts) | Selected-file preview lifecycle and stale-result protection. |
+| [`src/adapters/file-preview.ts`](../../../src/adapters/file-preview.ts) | Bounded filesystem preview reads. |
| [`src/app/workspace/file-tree.ts`](../../../src/app/workspace/file-tree.ts) | File tree state and entry behavior. |
diff --git a/spec/file-explorer-preview.spec.mjs b/spec/file-explorer-preview.spec.mjs
new file mode 100644
index 00000000..8e034bb1
--- /dev/null
+++ b/spec/file-explorer-preview.spec.mjs
@@ -0,0 +1,110 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { importDist, mockRuntime } from './workspace-helpers.mjs';
+
+const entries = [
+ { kind: 'file', name: 'alpha.txt', path: '/repo/alpha.txt' },
+ { kind: 'file', name: 'beta.txt', path: '/repo/beta.txt' },
+];
+const key = (key, flags = {}) => ({ type: 'key', key, ...flags });
+async function setup() {
+ const { createWorkspaceRuntime } = await importDist('app', 'workspace', 'runtime.js');
+ const { BijouI18nAdapter } = await importDist('adapters', 'bijou-i18n-adapter.js');
+ const reads = [];
+ const deps = mockRuntime();
+ deps.initialModel.entries = entries;
+ deps.initialModel.i18n = new BijouI18nAdapter();
+ const { availableJeditThemes } = await importDist('ui', 'jedit-themes.js');
+ deps.initialModel.jeditTheme = availableJeditThemes()[0];
+ deps.filePreviewSource = { loadFilePreview: (path) => {
+ reads.push(path);
+ return { kind: 'loaded', lines: [`PREVIEW ${path}`], truncated: false };
+ } };
+ const runtime = createWorkspaceRuntime(deps);
+ const model = { ...runtime.init()[0], startupIntroComplete: true };
+ return { runtime, model, reads };
+}
+function text(surface) {
+ return Array.from({ length: surface.height }, (_, y) => (
+ Array.from({ length: surface.width }, (_, x) => surface.get(x, y).char).join('')
+ )).join('\n');
+}
+
+test('selection previews in the main viewer without replacing editor state or opening an Echo buffer', async () => {
+ const { runtime, model, reads } = await setup();
+ const [opened, commands] = runtime.update(key('b', { ctrl: true }), model);
+ assert.equal(opened.focusPane, 'files');
+ assert.equal(opened.textAuthority, model.textAuthority);
+ assert.ok(commands.length > 0);
+ const message = await commands.at(-1)();
+ const [preview] = runtime.update(message, { ...opened, fileDrawerProgress: 1 });
+ assert.match(text(runtime.view(preview)), /Preview.*alpha.txt/);
+ assert.match(text(runtime.view(preview)), /PREVIEW.*alpha.txt/);
+ assert.deepEqual(reads, ['/repo/alpha.txt']);
+ assert.equal(preview.editor, model.editor);
+ assert.equal(preview.textAuthority, model.textAuthority);
+ assert.equal(preview.textRequestId, model.textRequestId);
+ assert.equal(preview.focusPane, 'files');
+ const [closed] = runtime.update(key('b', { ctrl: true }), preview);
+ assert.doesNotMatch(text(runtime.view(closed)), /PREVIEW/);
+});
+
+test('late preview results cannot paint a different selection and hover never reads a file', async () => {
+ const { runtime, model, reads } = await setup();
+ const [opened, first] = runtime.update(key('b', { ctrl: true }), model);
+ const [next, second] = runtime.update(key('j'), opened);
+ const firstMessage = await first.at(-1)();
+ const [late] = runtime.update(firstMessage, next);
+ assert.equal(late.fileExplorerPreview.entry.path, '/repo/beta.txt');
+ assert.equal(late.fileExplorerPreview.result, undefined);
+ const secondMessage = await second.at(-1)();
+ const [loaded] = runtime.update(secondMessage, next);
+ assert.equal(loaded.fileExplorerPreview.result.lines[0], 'PREVIEW /repo/beta.txt');
+ const [, hoverCommands] = runtime.update({ type: 'mouse', action: 'move', button: 'none', col: 5, row: 5 }, {
+ ...loaded, fileDrawerProgress: 1,
+ });
+ assert.deepEqual(hoverCommands, []);
+ assert.equal(reads.length, 2);
+});
+
+test('the active unsaved buffer projection is previewed without filesystem reads or mutation', async () => {
+ const { runtime, model, reads } = await setup();
+ const editor = { path: '/repo/alpha.txt', lines: ['UNSAVED CONTENT'], dirty: true };
+ const initial = { ...model, editor };
+ const [opened] = runtime.update(key('b', { ctrl: true }), initial);
+ assert.equal(opened.fileExplorerPreview.result.lines[0], 'UNSAVED CONTENT');
+ assert.deepEqual(reads, []);
+ assert.equal(opened.editor, editor);
+ assert.equal(opened.textAuthority, initial.textAuthority);
+});
+
+test('an open buffer previews its current reading rather than a stale opening window', async () => {
+ const { runtime, model, reads } = await setup();
+ const cache = { coverage: 'window', startLine: 0, lines: ['CURRENT'], totalLineCount: 500, truncated: false };
+ const textAuthority = { kind: 'opened', filePath: '/repo/alpha.txt', cache };
+ const initial = { ...model, editor: { path: '/repo/alpha.txt', lines: ['STALE'] }, textAuthority };
+ const [opened] = runtime.update(key('b', { ctrl: true }), initial);
+ assert.deepEqual(opened.fileExplorerPreview.result, { kind: 'loaded', lines: ['CURRENT'], truncated: true });
+ assert.deepEqual(reads, []);
+ const [scrolled, commands] = runtime.update(key('b', { ctrl: true }), {
+ ...initial, textAuthority: { ...textAuthority, cache: { ...cache, startLine: 40 } },
+ });
+ assert.equal(scrolled.fileExplorerPreview.result, undefined);
+ assert.ok(commands.length > 0, 'a reading that misses the head falls back to the file');
+});
+
+ test('click selection previews without opening and closing rejects its outstanding result', async () => {
+ const { runtime, model } = await setup();
+ const [opened] = runtime.update(key('b', { ctrl: true }), model);
+ const [selected, commands] = runtime.update({ type: 'mouse', action: 'press', button: 'left', col: 5, row: 6 }, {
+ ...opened, fileDrawerProgress: 1,
+ });
+ assert.equal(selected.selectedIndex, 1);
+ assert.equal(selected.focusPane, 'files');
+ assert.equal(selected.textRequestId, model.textRequestId);
+ const message = await commands.at(-1)();
+ const [closed] = runtime.update(key('b', { ctrl: true }), selected);
+ const [late] = runtime.update(message, closed);
+ assert.equal(late.fileExplorerPreview, undefined);
+ assert.equal(late.textAuthority, model.textAuthority);
+});
diff --git a/spec/file-explorer-search.spec.mjs b/spec/file-explorer-search.spec.mjs
new file mode 100644
index 00000000..6e8ef44b
--- /dev/null
+++ b/spec/file-explorer-search.spec.mjs
@@ -0,0 +1,301 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { QUIT } from '@flyingrobots/bijou-tui';
+import { importDist, mockRuntime } from './workspace-helpers.mjs';
+
+const ENTRIES = [
+ { kind: 'parent', name: '..', path: '/repo' },
+ { kind: 'dir', name: 'components', path: '/repo/src/components' },
+ { kind: 'file', name: 'alpha.txt', path: '/repo/src/alpha.txt' },
+ { kind: 'file', name: 'index.ts', path: '/repo/src/index.ts' },
+ { kind: 'file', name: 'render-tree.ts', path: '/repo/src/render-tree.ts' },
+ { kind: 'file', name: 'readme.md', path: '/repo/src/readme.md' },
+];
+const key = (key, flags = {}) => ({ type: 'key', key, ...flags });
+
+async function setup(options = {}) {
+ const { createWorkspaceRuntime } = await importDist('app', 'workspace', 'runtime.js');
+ const { BijouI18nAdapter } = await importDist('adapters', 'bijou-i18n-adapter.js');
+ const { availableJeditThemes } = await importDist('ui', 'jedit-themes.js');
+ const deps = mockRuntime(options.deps);
+ deps.initialModel.entries = options.entries ?? ENTRIES;
+ deps.initialModel.i18n = new BijouI18nAdapter().withLocale(options.locale ?? 'en');
+ deps.initialModel.jeditTheme = availableJeditThemes()[0];
+ const runtime = createWorkspaceRuntime(deps);
+ const initial = { ...runtime.init()[0], cwd: '/repo/src', startupIntroComplete: true };
+ const [opened] = runtime.update(key('b', { ctrl: true }), initial);
+ const focused = options.navigation ? opened : runtime.update(key('/'), opened)[0];
+ return { runtime, initial, model: { ...focused, fileDrawerProgress: 1 } };
+}
+
+function type(runtime, model, input) {
+ for (const character of input) model = runtime.update(key(character), model)[0];
+ return model;
+}
+
+function text(surface) {
+ return Array.from({ length: surface.height }, (_, y) => (
+ Array.from({ length: surface.width }, (_, x) => surface.get(x, y).char).join('')
+ )).join('\n');
+}
+
+async function drawer(model, width = 32, height = 12) {
+ const { renderDrawer } = await importDist('app', 'workspace', 'viewer-drawers.js');
+ return renderDrawer('files', model, width, height);
+}
+
+test('slash focuses a sticky search input with a visible caret', async () => {
+ const { model } = await setup();
+ assert.equal(model.focusPane, 'files');
+ assert.deepEqual(model.fileSearch, { input: '', cursorIndex: 0 });
+ const surface = await drawer(model);
+ assert.match(text(surface).split('\n')[1], /Search: Filter files/);
+ assert.equal(surface.get(9, 1).bg, model.jeditTheme.cursor.insert.bg);
+});
+
+test('typing fuzzy-filters loaded relative paths without mutating the source list', async () => {
+ const { runtime, model } = await setup();
+ const next = type(runtime, model, 'SRC/RNT');
+ assert.equal(next.fileSearch.input, 'SRC/RNT');
+ assert.equal(next.entries, model.entries);
+ assert.equal(next.selectedIndex, 4);
+ const rendered = text(await drawer(next));
+ assert.match(rendered, /render-tree\.ts/);
+ assert.doesNotMatch(rendered, /alpha\.txt|index\.ts|readme\.md|components\/|\.\.\//);
+});
+
+test('plain shortcut characters belong to the focused search field while Ctrl+C still quits', async () => {
+ const { runtime, model } = await setup();
+ let next = model;
+ for (const character of 'qjkrh:l` ') {
+ const [updated, commands] = runtime.update(key(character), next);
+ assert.deepEqual(commands, []);
+ assert.equal(updated.quitConfirmOpen, false);
+ assert.equal(updated.perfVisible, false);
+ next = updated;
+ }
+ assert.equal(next.fileSearch.input, 'qjkrh:l` ');
+ const [, commands] = runtime.update(key('c', { ctrl: true }), next);
+ assert.equal(await commands[0](), QUIT);
+});
+
+test('the query caret edits Unicode text and supports clearing without leaving the directory', async () => {
+ const { runtime, model } = await setup();
+ let next = type(runtime, model, 'a🦀界');
+ next = runtime.update(key('left'), next)[0];
+ next = runtime.update(key('backspace'), next)[0];
+ assert.deepEqual(next.fileSearch, { input: 'a界', cursorIndex: 1 });
+ next = runtime.update(key('delete'), next)[0];
+ assert.deepEqual(next.fileSearch, { input: 'a', cursorIndex: 1 });
+ next = runtime.update(key('home'), next)[0];
+ next = type(runtime, next, 'é');
+ assert.equal(next.fileSearch.input, 'éa');
+ next = runtime.update(key('u', { ctrl: true }), next)[0];
+ assert.deepEqual(next.fileSearch, { input: '', cursorIndex: 0 });
+ assert.equal(next.cwd, '/repo/src');
+ next = runtime.update(key('backspace'), next)[0];
+ assert.equal(next.cwd, '/repo/src');
+});
+
+test('arrows select visible matches and Enter opens the displayed file, including no-match safety', async () => {
+ const { runtime, model } = await setup();
+ let next = type(runtime, model, '.ts');
+ assert.equal(next.selectedIndex, 3);
+ next = runtime.update(key('down'), next)[0];
+ assert.equal(next.selectedIndex, 4);
+ next = runtime.update(key('escape'), next)[0];
+ const [opening, commands] = runtime.update(key('enter'), next);
+ assert.equal(opening.textAuthority.filePath, '/repo/src/render-tree.ts');
+ assert.equal(commands.length, 1);
+ const empty = type(runtime, model, 'not-a-match');
+ assert.match(text(await drawer(empty)), /No matches/);
+ const [unchanged, none] = runtime.update(key('enter'), empty);
+ assert.equal(unchanged.textAuthority, empty.textAuthority);
+ assert.equal(unchanged.selectedIndex, -1);
+ assert.deepEqual(none, []);
+});
+
+test('the input stays pinned while the filtered list scrolls and caret text scrolls horizontally', async () => {
+ const entries = Array.from({ length: 30 }, (_, i) => ({ kind: 'file', name: `entry-${i}.ts`, path: `/repo/src/entry-${i}.ts` }));
+ const { runtime, model } = await setup({ entries });
+ let next = type(runtime, model, 'en');
+ const initialHeader = text(await drawer(next, 28, 10)).split('\n')[1];
+ for (let i = 0; i < 22; i += 1) next = runtime.update(key('down'), next)[0];
+ const rendered = text(await drawer(next, 28, 10)).split('\n');
+ assert.equal(rendered[1], initialHeader);
+ assert.ok(rendered.some((line) => line.includes('› entry-22.ts')));
+ next = runtime.update(key('u', { ctrl: true }), next)[0];
+ next = type(runtime, next, 'a'.repeat(40) + 'tail');
+ assert.match(text(await drawer(next, 28, 10)).split('\n')[1], /tail/);
+});
+
+test('filtered result clicks and wheel selection use the pinned-header geometry', async () => {
+ const { runtime, model } = await setup();
+ const filtered = type(runtime, model, '.ts');
+ const [clicked] = runtime.update({ type: 'mouse', action: 'press', button: 'left', col: 5, row: 6 }, filtered);
+ assert.equal(clicked.selectedIndex, 4);
+ assert.equal(clicked.textAuthority, model.textAuthority);
+ const [wheeled] = runtime.update({ type: 'mouse', action: 'scroll-down', button: 'none', col: 5, row: 8 }, filtered);
+ assert.equal(wheeled.selectedIndex, 4);
+ const [inputClick, commands] = runtime.update({ type: 'mouse', action: 'press', button: 'left', col: 10, row: 3 }, {
+ ...filtered, focusPane: 'editor',
+ });
+ assert.equal(inputClick.focusPane, 'files');
+ assert.deepEqual(commands, []);
+ assert.equal(inputClick.textAuthority, model.textAuthority);
+});
+
+test('close/reopen retains the query, directory changes clear it, and failed changes retain it', async () => {
+ const { runtime, model } = await setup({ deps: { fileSystem: {
+ loadEntries: (cwd) => { if (cwd === '/repo/src/components') throw new Error('unreadable'); return ENTRIES; },
+ dirname: () => '/repo', join: (...parts) => parts.join('/'), resolve: (...parts) => parts.join('/'),
+ describeDirectoryIssue: () => ({ title: 'Directory failed', message: 'unreadable' }),
+ } } });
+ let next = type(runtime, model, 'rnt');
+ next = runtime.update(key('b', { ctrl: true }), next)[0];
+ assert.equal(next.fileDrawerOpen, false);
+ next = runtime.update(key('b', { ctrl: true }), next)[0];
+ assert.equal(next.focusPane, 'files');
+ assert.equal(next.fileSearch.input, 'rnt');
+ next = runtime.update(key('up', { alt: true }), next)[0];
+ assert.equal(next.cwd, '/repo');
+ assert.equal(next.fileSearch.input, '');
+ const failed = type(runtime, model, 'components');
+ const [after] = runtime.update(key('enter'), runtime.update(key('escape'), failed)[0]);
+ assert.equal(after.cwd, '/repo/src');
+ assert.equal(after.fileSearch.input, 'components');
+ assert.equal(after.entries, model.entries);
+});
+
+test('Settings and pane focus take ownership without changing the search query', async () => {
+ const { runtime, model } = await setup();
+ const filtered = type(runtime, model, 'rnt');
+ const settings = runtime.update(key('f2'), filtered)[0];
+ assert.equal(settings.settingsOpen, true);
+ assert.equal(type(runtime, settings, 'q').fileSearch.input, 'rnt');
+ const unfocused = { ...filtered, focusPane: 'editor' };
+ const surface = await drawer(unfocused);
+ assert.equal(surface.get(12, 1).bg, model.jeditTheme.surface.currentLine.bg);
+ assert.equal(unfocused.fileSearch.input, 'rnt');
+});
+
+test('refresh keeps the query and no-match pointer actions cannot open a hidden file', async () => {
+ const { runtime, model } = await setup();
+ const filtered = type(runtime, model, 'rnt');
+ const refreshed = runtime.update(key('r', { ctrl: true }), filtered)[0];
+ assert.deepEqual(refreshed.fileSearch, filtered.fileSearch);
+ const empty = type(runtime, model, 'zzzzzz');
+ for (const action of ['press', 'scroll-down', 'scroll-up']) {
+ const [next, commands] = runtime.update({ type: 'mouse', action, button: 'left', col: 5, row: 6 }, empty);
+ assert.equal(next.selectedIndex, -1);
+ assert.equal(next.textAuthority, empty.textAuthority);
+ assert.deepEqual(commands, []);
+ }
+});
+
+test('localized labels fit narrow fields and wide-character clicks use display columns', async () => {
+ const { fileSearchCursorAtColumn } = await importDist('ui', 'file-search-input.js');
+ const { fileSearchInputOptions } = await importDist('app', 'workspace', 'file-explorer-view.js');
+ for (const locale of ['en', 'ja', 'ar', 'me']) {
+ const { runtime, model } = await setup({ locale });
+ const next = type(runtime, model, '界x');
+ const options = fileSearchInputOptions(next, 28);
+ const surface = await drawer(next, 30, 8);
+ assert.match(text(surface).split('\n')[1], /界x/);
+ assert.ok(!options.label.includes('fileExplorer.'));
+ const { visibleLineLength } = await importDist('ui', 'fit-line.js');
+ const prefix = visibleLineLength(`${options.label}: `);
+ assert.equal(fileSearchCursorAtColumn(options, prefix + 2), 1);
+ assert.equal(fileSearchCursorAtColumn(options, prefix + 3), 2);
+ }
+});
+
+test('the input has a distinct background and only fuzzy-matched letters receive the accent', async () => {
+ const { runtime, model } = await setup({ entries: [ENTRIES[4], { ...ENTRIES[4], name: 'render-test.ts', path: '/repo/src/render-test.ts' }] });
+ const filtered = type(runtime, model, 'rnt');
+ const surface = await drawer(filtered);
+ assert.notEqual(surface.get(2, 1).bg, surface.get(2, 3).bg);
+ assert.equal(surface.get(2, 1).bg, model.jeditTheme.surface.currentLine.bg);
+ for (const x of [3, 5, 10]) {
+ assert.equal(surface.get(x, 3).fg, surface.get(4, 3).fg);
+ assert.equal(surface.get(x, 4).fg, model.jeditTheme.chrome.activeEdge.fg);
+ assert.ok(surface.get(x, 3).modifiers.includes('bold'));
+ }
+ assert.equal(surface.get(4, 4).fg, model.jeditTheme.surface.drawer.fg);
+ const cleared = runtime.update(key('u', { ctrl: true }), filtered)[0];
+ const plain = await drawer(cleared);
+ assert.equal(plain.get(3, 4).fg, model.jeditTheme.surface.drawer.fg);
+});
+
+test('the input background remains distinct across built-in dark and light themes', async () => {
+ const { availableJeditThemes, oppositeJeditTheme } = await importDist('ui', 'jedit-themes.js');
+ const { model } = await setup();
+ for (const theme of availableJeditThemes().flatMap((theme) => [theme, oppositeJeditTheme(theme)])) {
+ const surface = await drawer({ ...model, jeditTheme: theme });
+ assert.notEqual(surface.get(2, 1).bg, surface.get(2, 3).bg, theme.name);
+ }
+});
+
+ test('Files opens in navigation mode, slash edits, and Escape returns to navigation before closing', async () => {
+ const { runtime, model } = await setup({ navigation: true });
+ assert.equal(model.fileSearchEditing, false);
+ let next = runtime.update(key('j'), model)[0];
+ assert.equal(next.selectedIndex, 1);
+ assert.equal(next.fileSearch.input, '');
+ next = runtime.update(key('k'), next)[0];
+ assert.equal(next.selectedIndex, 0);
+ next = runtime.update(key('/'), next)[0];
+ assert.equal(next.fileSearchEditing, true);
+ next = type(runtime, next, '.ts');
+ next = runtime.update(key('escape'), next)[0];
+ assert.equal(next.fileDrawerOpen, true);
+ assert.equal(next.fileSearchEditing, false);
+ assert.equal(next.fileSearch.input, '.ts');
+ next = runtime.update(key('j'), next)[0];
+ assert.equal(next.selectedIndex, 4);
+ next = runtime.update(key('escape'), next)[0];
+ assert.equal(next.fileDrawerOpen, false);
+});
+
+ test('mouse hover is separate from selection and clicking selects without opening', async () => {
+ const { runtime, model } = await setup({ navigation: true });
+ const [hover, commands] = runtime.update({ type: 'mouse', action: 'move', button: 'none', col: 5, row: 7 }, model);
+ assert.equal(hover.fileHoveredIndex, 2);
+ assert.equal(hover.selectedIndex, 0);
+ assert.deepEqual(commands, []);
+ const surface = await drawer(hover);
+ assert.notEqual(surface.get(2, 3).bg, surface.get(2, 5).bg);
+ const [selected] = runtime.update({ type: 'mouse', action: 'press', button: 'left', col: 5, row: 7 }, hover);
+ assert.equal(selected.selectedIndex, 2);
+ assert.equal(selected.focusPane, 'files');
+ assert.equal(selected.textAuthority, model.textAuthority);
+ const [outside] = runtime.update({ type: 'mouse', action: 'move', button: 'none', col: 80, row: 7 }, selected);
+ assert.equal(outside.fileHoveredIndex, undefined);
+});
+
+ test('portable paths from the filesystem boundary exclude drive and workspace text from filtering', async () => {
+ const { portableFileSearchPath } = await importDist('adapters', 'filesystem.js');
+ const { runtime, model } = await setup({ entries: [{
+ kind: 'file', name: 'index.ts', path: 'C:\\repo\\src\\index.ts',
+ searchPath: portableFileSearchPath('C:\\repo\\src\\index.ts'),
+ }] });
+ const initial = { ...model, workspaceRoot: 'C:\\repo', workspaceSearchRoot: portableFileSearchPath('C:\\repo') };
+ assert.equal(type(runtime, initial, 'src/index').selectedIndex, 0);
+ assert.equal(type(runtime, initial, 'repo').selectedIndex, -1);
+ const above = { ...initial, entries: [{ kind: 'file', name: 'notes.md', path: 'C:\\repo\\notes.md', searchPath: portableFileSearchPath('C:\\repo\\notes.md') }] };
+ assert.equal(type(runtime, { ...above, workspaceSearchRoot: portableFileSearchPath('C:\\repo\\src') }, 'repo').selectedIndex, -1);
+ assert.equal(type(runtime, above, 'notes').selectedIndex, 0);
+});
+
+ test('selected rows remain readable and distinct from hover across dark and light themes', async () => {
+ const { availableJeditThemes, oppositeJeditTheme } = await importDist('ui', 'jedit-themes.js');
+ const { contrastRatio } = await importDist('ui', 'theme-contrast.js');
+ const { model } = await setup({ navigation: true });
+ for (const theme of availableJeditThemes().flatMap((theme) => [theme, oppositeJeditTheme(theme)])) {
+ const surface = await drawer({ ...model, jeditTheme: theme, selectedIndex: 1, fileHoveredIndex: 2 });
+ const selected = surface.get(3, 4);
+ const hovered = surface.get(3, 5);
+ assert.notEqual(selected.bg, hovered.bg, theme.name);
+ assert.ok(contrastRatio(selected.fgRGB, selected.bgRGB) >= 4.45, theme.name);
+ }
+});
diff --git a/spec/file-preview-source.spec.mjs b/spec/file-preview-source.spec.mjs
new file mode 100644
index 00000000..646f26a0
--- /dev/null
+++ b/spec/file-preview-source.spec.mjs
@@ -0,0 +1,44 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { mkdtempSync, writeFileSync } from 'node:fs';
+import { join } from 'node:path';
+import { tmpdir } from 'node:os';
+import { spawnSync } from 'node:child_process';
+import { importDist } from './dist-helpers.mjs';
+
+test('filesystem previews bound bytes and lines and report binary or unavailable files', async () => {
+ const { filePreviewSource } = await importDist('adapters', 'file-preview.js');
+ const dir = mkdtempSync(join(tmpdir(), 'jedit-preview-spec-'));
+ const large = join(dir, 'large.txt');
+ writeFileSync(large, '界'.repeat(40000));
+ const truncated = filePreviewSource.loadFilePreview(large);
+ assert.equal(truncated.kind, 'loaded');
+ assert.equal(truncated.truncated, true);
+ assert.ok(Buffer.byteLength(truncated.lines.join('\n')) <= 65536);
+ assert.ok(!truncated.lines.join('').includes('�'));
+ const many = join(dir, 'many.txt');
+ writeFileSync(many, 'line\n'.repeat(250));
+ const lines = filePreviewSource.loadFilePreview(many);
+ assert.equal(lines.lines.length, 200);
+ assert.equal(lines.truncated, true);
+ const binary = join(dir, 'binary');
+ writeFileSync(binary, Buffer.from([0, 1, 2]));
+ assert.equal(filePreviewSource.loadFilePreview(binary).kind, 'binary');
+ assert.equal(filePreviewSource.loadFilePreview(join(dir, 'missing')).kind, 'unavailable');
+ assert.equal(filePreviewSource.loadFilePreview(dir).kind, 'unavailable');
+});
+
+ test('a named pipe cannot block the preview reader', async (context) => {
+ const { filePreviewSource } = await importDist('adapters', 'file-preview.js');
+ const dir = mkdtempSync(join(tmpdir(), 'jedit-preview-pipe-'));
+ const pipe = join(dir, 'pipe');
+ const created = spawnSync('mkfifo', [pipe]);
+ if (created.error?.code === 'ENOENT') { context.skip('mkfifo unavailable'); return; }
+ assert.equal(created.status, 0);
+ const script = `import {filePreviewSource} from ${JSON.stringify(new URL('../dist/adapters/file-preview.js', import.meta.url).href)};` +
+ `process.stdout.write(JSON.stringify(filePreviewSource.loadFilePreview(${JSON.stringify(pipe)})));`;
+ const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], { timeout: 1000, encoding: 'utf8' });
+ assert.equal(result.error, undefined);
+ assert.equal(JSON.parse(result.stdout).kind, 'unavailable');
+ assert.equal(filePreviewSource.loadFilePreview(dir).kind, 'unavailable');
+});
diff --git a/spec/terminal-input-stream.spec.mjs b/spec/terminal-input-stream.spec.mjs
new file mode 100644
index 00000000..14373fe0
--- /dev/null
+++ b/spec/terminal-input-stream.spec.mjs
@@ -0,0 +1,36 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { setTimeout as delay } from 'node:timers/promises';
+import { importDist } from './workspace-helpers.mjs';
+
+const ESCAPE_SETTLE_MS = 60;
+
+async function stream() {
+ const { terminalInputStream } = await importDist('adapters', 'terminal-input-stream.js');
+ const tokens = [];
+ return { tokens, input: terminalInputStream((token) => tokens.push(token)) };
+}
+
+test('complete escape sequences and printable keys are split into tokens', async () => {
+ const { tokens, input } = await stream();
+ input.accept('a\u001b[Ab');
+ assert.deepEqual(tokens, ['a', '\u001b[A', 'b']);
+ input.dispose();
+});
+
+test('an unfinished CSI prefix is flushed after the escape delay', async () => {
+ const { tokens, input } = await stream();
+ input.accept('\u001b[');
+ await delay(ESCAPE_SETTLE_MS);
+ input.accept('\u0003');
+ assert.deepEqual(tokens, ['\u001b[', '\u0003']);
+ input.dispose();
+});
+
+test('control keys inside an unfinished CSI prefix are delivered on their own', async () => {
+ const { tokens, input } = await stream();
+ input.accept('\u001b[');
+ input.accept('\u0003');
+ assert.deepEqual(tokens, ['\u001b[', '\u0003']);
+ input.dispose();
+});
diff --git a/spec/workspace-drawer-scroll.spec.mjs b/spec/workspace-drawer-scroll.spec.mjs
index 48b0c3ec..86fd8de8 100644
--- a/spec/workspace-drawer-scroll.spec.mjs
+++ b/spec/workspace-drawer-scroll.spec.mjs
@@ -80,10 +80,10 @@ test("a click maps through the scroll offset to the right entry", async () => {
rows: model.rows,
footerVisible: model.footerVisible,
});
- const listHeight = bodyHeight - (viewport.DRAWER_INNER_PAD * 2);
+ const listHeight = viewport.fileSearchListHeight(bodyHeight);
const offset = viewport.listScrollOffset(60, entries.length, listHeight);
const firstRow =
- viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD;
+ viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP;
const [next] = mouse.updateFromMouse(
{
@@ -103,7 +103,7 @@ test("a click maps through the scroll offset to the right entry", async () => {
assert.equal(next.selectedIndex, offset);
});
-test("clicking a file opens it, not just selects it", async () => {
+test("clicking a file selects it and leaves opening to Enter", async () => {
const [mouse, viewport, panelFocus, titleScreen] = await Promise.all([
importDist("app", "workspace", "mouse.js"),
importDist("app", "workspace", "viewport.js"),
@@ -126,9 +126,9 @@ test("clicking a file opens it, not just selects it", async () => {
columns: 100,
rows: 30,
});
- const row = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD + 1;
+ const row = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP + 1;
- mouse.updateFromMouse(
+ const [selected] = mouse.updateFromMouse(
{ type: "mouse", button: "left", action: "press", col: 2, row, shift: false, alt: false, ctrl: false },
model,
{ highlight: () => undefined },
@@ -141,5 +141,6 @@ test("clicking a file opens it, not just selects it", async () => {
},
);
- assert.deepEqual(opened, ["/w/beta.txt"]);
+ assert.equal(selected.selectedIndex, 1);
+ assert.deepEqual(opened, []);
});
diff --git a/spec/workspace-footer.spec.mjs b/spec/workspace-footer.spec.mjs
index c744ce15..56828db1 100644
--- a/spec/workspace-footer.spec.mjs
+++ b/spec/workspace-footer.spec.mjs
@@ -263,7 +263,7 @@ test('workspace footer shows file drawer controls and the selected file path', a
graftSelection: undefined,
}),
[
- 'FILES [j/k move · enter open · backspace up · ctrl+b close · tab focus]',
+ 'FILES [j/k move · slash filter · enter open · ctrl+b close · tab focus]',
'/repo/notes/very-long-file-name.md',
],
);
@@ -380,7 +380,7 @@ test('workspace footer obtains command hints from i18n', async () => {
});
assert.equal(primary.includes(''), true);
- assert.equal(primary.includes(''), true);
+ assert.equal(primary.includes(''), true);
assert.equal(primary.includes(''), true);
assert.equal(requestedKeys.includes('footer.hints.enter_open'), true);
assert.equal(requestedKeys.includes('footer.hints.ctrl_b_close'), true);
diff --git a/spec/workspace-list-navigation.spec.mjs b/spec/workspace-list-navigation.spec.mjs
index b45d3cfd..bd15ec46 100644
--- a/spec/workspace-list-navigation.spec.mjs
+++ b/spec/workspace-list-navigation.spec.mjs
@@ -80,7 +80,7 @@ test("the file explorer selection wraps from the last entry to the first", async
const model = await fileDrawerModel({ selectedIndex: LAST_INDEX });
const [next] = keyBindings.updateFromKey(
- { key: "j" },
+ { key: "down" },
model,
mockKeyBindingContext(),
);
@@ -93,7 +93,7 @@ test("the file explorer selection wraps from the first entry to the last", async
const model = await fileDrawerModel({ selectedIndex: FIRST_INDEX });
const [next] = keyBindings.updateFromKey(
- { key: "k" },
+ { key: "up" },
model,
mockKeyBindingContext(),
);
@@ -106,7 +106,7 @@ test("an empty file explorer stays put instead of wrapping onto nothing", async
const model = await fileDrawerModel({ entries: [], selectedIndex: 0 });
const [next] = keyBindings.updateFromKey(
- { key: "j" },
+ { key: "down" },
model,
mockKeyBindingContext(),
);
@@ -180,7 +180,7 @@ test("clicking a file explorer row selects that entry", async () => {
importDist("app", "workspace", "viewport.js"),
]);
const model = await fileDrawerModel({ columns: 100, rows: 30, fileDrawerProgress: 1 });
- const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD + 2;
+ const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP + 2;
const [next] = mouse.updateFromMouse(
{ type: "mouse", button: "left", action: "press", col: 2, row: clickRow, shift: false, alt: false, ctrl: false },
@@ -197,7 +197,7 @@ test("clicking outside the file explorer leaves the selection alone", async () =
importDist("app", "workspace", "viewport.js"),
]);
const model = await fileDrawerModel({ columns: 100, rows: 30, fileDrawerProgress: 1, selectedIndex: 1 });
- const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD + 2;
+ const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP + 2;
const [next] = mouse.updateFromMouse(
{ type: "mouse", button: "left", action: "press", col: 95, row: clickRow, shift: false, alt: false, ctrl: false },
@@ -214,7 +214,7 @@ test("clicking past the last entry leaves the selection alone", async () => {
importDist("app", "workspace", "viewport.js"),
]);
const model = await fileDrawerModel({ columns: 100, rows: 30, fileDrawerProgress: 1, selectedIndex: 1 });
- const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD + 20;
+ const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP + 20;
const [next] = mouse.updateFromMouse(
{ type: "mouse", button: "left", action: "press", col: 2, row: clickRow, shift: false, alt: false, ctrl: false },
diff --git a/spec/workspace-terminal-input.spec.mjs b/spec/workspace-terminal-input.spec.mjs
new file mode 100644
index 00000000..533756a5
--- /dev/null
+++ b/spec/workspace-terminal-input.spec.mjs
@@ -0,0 +1,43 @@
+import assert from 'node:assert/strict';
+import test from 'node:test';
+import { createEventBus } from '@flyingrobots/bijou-tui';
+import { importDist } from './dist-helpers.mjs';
+
+ test('the production input boundary emits Unicode, modified arrows, and hover through the Bijou event bus', async () => {
+ const { workspaceTerminalIO } = await importDist('adapters', 'workspace-terminal-io.js');
+ let input;
+ let disposed = false;
+ const rawIO = { rawInput: (handler) => {
+ input = handler;
+ return { dispose: () => { disposed = true; } };
+ }, onResize: () => ({ dispose() {} }) };
+ const bus = createEventBus();
+ const received = [];
+ bus.on((msg) => received.push(msg));
+ const connection = bus.connectIO(workspaceTerminalIO(rawIO), { mouse: true });
+ input('/é界🦀');
+ input('\x1b[1;3A');
+ input('\x1b[<35;6;8M');
+ assert.deepEqual(received.map((msg) => msg.key ?? msg.action), ['/', 'é', '界', '🦀', 'up', 'move']);
+ assert.equal(received[4].alt, true);
+ assert.equal(received[5].col, 5);
+ assert.equal(received[5].row, 7);
+ connection.dispose();
+ assert.equal(disposed, true);
+});
+
+ test('split escape sequences stay intact, ordinary keys preserve modifiers, and lone Escape is delivered', async () => {
+ const { workspaceTerminalIO } = await importDist('adapters', 'workspace-terminal-io.js');
+ let input;
+ const rawIO = { rawInput: (handler) => { input = handler; return { dispose() {} }; }, onResize: () => ({ dispose() {} }) };
+ const bus = createEventBus();
+ const received = [];
+ bus.on((msg) => received.push(msg));
+ const connection = bus.connectIO(workspaceTerminalIO(rawIO), { mouse: true });
+ input('\x1b[1;'); input('3A'); input('J\x02'); input('\x1b');
+ await new Promise((resolve) => setTimeout(resolve, 40));
+ assert.deepEqual(received.map((msg) => msg.key), ['up', 'j', 'b', 'escape']);
+ assert.equal(received[1].shift, true);
+ assert.equal(received[2].ctrl, true);
+ connection.dispose();
+});
diff --git a/src/adapters/file-preview.ts b/src/adapters/file-preview.ts
new file mode 100644
index 00000000..97c104df
--- /dev/null
+++ b/src/adapters/file-preview.ts
@@ -0,0 +1,30 @@
+import { constants, closeSync, fstatSync, openSync, readSync } from 'node:fs';
+import { normalizeLines } from '../app/editor-lines.js';
+import { FilePreviewKinds, FILE_PREVIEW_MAX_BYTES, FILE_PREVIEW_MAX_LINES, type FilePreviewSource } from '../ports/file-preview.js';
+
+const UTF8_ENCODING = 'utf-8';
+const NULL_BYTE = 0;
+
+export const filePreviewSource: FilePreviewSource = {
+ loadFilePreview(path) {
+ let descriptor: number | undefined;
+ try {
+ descriptor = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK);
+ const stat = fstatSync(descriptor);
+ if (!stat.isFile()) return { kind: FilePreviewKinds.Unavailable };
+ const bytes = Buffer.alloc(FILE_PREVIEW_MAX_BYTES);
+ const read = readSync(descriptor, bytes, 0, bytes.length, 0);
+ const content = bytes.subarray(0, read);
+ if (content.includes(NULL_BYTE)) return { kind: FilePreviewKinds.Binary };
+ const lines = normalizeLines(new TextDecoder(UTF8_ENCODING).decode(content, { stream: read < stat.size }));
+ return {
+ kind: FilePreviewKinds.Loaded, lines: lines.slice(0, FILE_PREVIEW_MAX_LINES),
+ truncated: read < stat.size || lines.length > FILE_PREVIEW_MAX_LINES,
+ };
+ } catch {
+ return { kind: FilePreviewKinds.Unavailable };
+ } finally {
+ if (descriptor != null) closeSync(descriptor);
+ }
+ },
+};
diff --git a/src/adapters/filesystem.ts b/src/adapters/filesystem.ts
index ba0a2f64..96ca61d1 100644
--- a/src/adapters/filesystem.ts
+++ b/src/adapters/filesystem.ts
@@ -52,6 +52,7 @@ export function loadEntries(cwd: string): readonly FileEntry[] {
kind: 'parent',
name: '..',
path: parent,
+ searchPath: portableFileSearchPath(parent),
});
}
@@ -60,6 +61,7 @@ export function loadEntries(cwd: string): readonly FileEntry[] {
kind: entry.isDirectory() ? 'dir' : 'file',
name: entry.name,
path: join(cwd, entry.name),
+ searchPath: portableFileSearchPath(join(cwd, entry.name)),
}))
.sort(compareEntries);
@@ -129,3 +131,7 @@ function formatUnknownFilesystemError(cause: Throwable): string {
? cause.message
: String(cause);
}
+
+export function portableFileSearchPath(path: string): string {
+ return path.replaceAll('\\', '/');
+}
diff --git a/src/adapters/terminal-input-stream.ts b/src/adapters/terminal-input-stream.ts
new file mode 100644
index 00000000..006ad8df
--- /dev/null
+++ b/src/adapters/terminal-input-stream.ts
@@ -0,0 +1,54 @@
+const ESCAPE = '\u001b';
+const CSI_START = `${ESCAPE}[`;
+const SS3_START = `${ESCAPE}O`;
+const CSI_FINAL = /[@-~]/u;
+const ESCAPE_DELAY_MS = 25;
+const FIRST_SEQUENCE_CODE = 0x20;
+const LAST_SEQUENCE_CODE = 0x7e;
+
+export function terminalInputStream(emit: (token: string) => void) {
+ let pending = '';
+ let timer: ReturnType | undefined;
+ const clearTimer = () => { if (timer != null) clearTimeout(timer); };
+ return {
+ accept(raw: string) {
+ clearTimer();
+ pending += raw;
+ while (pending.length > 0) {
+ const length = terminalTokenLength(pending);
+ if (length == null) break;
+ emit(pending.slice(0, length));
+ pending = pending.slice(length);
+ }
+ // A bare Escape or an unfinished sequence (e.g. Alt+[ sending ESC[) is
+ // flushed after the delay so it can never hold later input hostage.
+ if (pending.length > 0) {
+ timer = setTimeout(() => { const flushed = pending; pending = ''; emit(flushed); }, ESCAPE_DELAY_MS);
+ }
+ },
+ dispose() { clearTimer(); pending = ''; },
+ };
+}
+
+function terminalTokenLength(raw: string): number | undefined {
+ if (raw === ESCAPE) return undefined;
+ if (raw.startsWith(CSI_START)) return csiTokenLength(raw);
+ if (raw.startsWith(SS3_START)) return raw.length >= 3 ? 3 : undefined;
+ return Array.from(raw)[0]?.length;
+}
+
+function csiTokenLength(raw: string): number | undefined {
+ for (let index = CSI_START.length; index < raw.length; index += 1) {
+ const character = raw[index] ?? '';
+ // A control byte (Ctrl+C, Escape) cannot belong to a CSI sequence: end the
+ // unfinished prefix there so the control key is delivered on its own.
+ if (!isSequenceByte(character)) return index;
+ if (CSI_FINAL.test(character)) return index + 1;
+ }
+ return undefined;
+}
+
+function isSequenceByte(character: string): boolean {
+ const code = character.charCodeAt(0);
+ return code >= FIRST_SEQUENCE_CODE && code <= LAST_SEQUENCE_CODE;
+}
diff --git a/src/adapters/workspace-app.ts b/src/adapters/workspace-app.ts
index e1a90503..a84a951f 100644
--- a/src/adapters/workspace-app.ts
+++ b/src/adapters/workspace-app.ts
@@ -1,3 +1,4 @@
+import { filePreviewSource } from './file-preview.js';
import { FileSystemPortAdapter } from './filesystem.js';
import { createWorkspaceRuntime } from '../app/workspace/runtime.js';
import { createRaytracerProfilerPort } from './raytracer-profiler.js';
@@ -16,8 +17,6 @@ import {
createWorkspaceNotificationTickCmd,
createWorkspaceTimeTickCmd,
} from './workspace-animation-commands.js';
-import { openWorkspaceFileEntry } from '../app/workspace/file-tree.js';
-import type { WorkspaceModel } from '../app/workspace/model.js';
export interface WorkspaceAppOptions {
initialColumns: number;
@@ -54,6 +53,7 @@ function workspaceRuntimeDependencies(
const graftSession = createGraftSessionPort();
const sourceHighlighter = createGraftSourceHighlighter();
return {
+ filePreviewSource,
initialColumns: options.initialColumns,
initialRows: options.initialRows,
initialWorkingDirectory: options.initialWorkingDirectory,
@@ -67,16 +67,6 @@ function workspaceRuntimeDependencies(
profileOnStartup: options.profileEnabled ?? false,
initialModel: options.seed ?? createInitialModelSnapshot(nowMs(), options.initialWorkingDirectory),
nowMs,
- openEntry: (
- model: WorkspaceModel,
- entry: NonNullable,
- ) => openWorkspaceFileEntry(model, entry, nowMs, {
- fileSystem: FileSystemPortAdapter,
- editorFile,
- graftSession,
- sourceHighlighter,
- productionTextSession: productionText.productionTextSession,
- }),
createTimeTickCmd: createWorkspaceTimeTickCmd,
createNotificationTickCmd: createWorkspaceNotificationTickCmd,
createDrawerAnimationCmd: createWorkspaceDrawerAnimationCmd,
diff --git a/src/adapters/workspace-initial-model-snapshot.ts b/src/adapters/workspace-initial-model-snapshot.ts
index 251d7fed..3d5ec6d0 100644
--- a/src/adapters/workspace-initial-model-snapshot.ts
+++ b/src/adapters/workspace-initial-model-snapshot.ts
@@ -4,7 +4,7 @@ import {
JEDIT_THEME_ENV,
resolveInitialJeditTheme,
} from "../ui/jedit-themes.js";
-import { loadEntries } from "./filesystem.js";
+import { loadEntries, portableFileSearchPath } from "./filesystem.js";
export function createInitialModelSnapshot(
nowMs: number,
@@ -12,6 +12,7 @@ export function createInitialModelSnapshot(
): WorkspaceInitialModelSnapshot {
return {
entries: loadEntries(cwd),
+ searchRoot: portableFileSearchPath(cwd),
jeditTheme: resolveInitialJeditTheme(process.env[JEDIT_THEME_ENV]),
i18n: new BijouI18nAdapter(),
nowMs,
diff --git a/src/adapters/workspace-terminal-io.ts b/src/adapters/workspace-terminal-io.ts
new file mode 100644
index 00000000..294d7f5d
--- /dev/null
+++ b/src/adapters/workspace-terminal-io.ts
@@ -0,0 +1,46 @@
+import type { IOPort } from '@flyingrobots/bijou';
+import type { KeyMsg } from '@flyingrobots/bijou-tui';
+import { terminalInputStream } from './terminal-input-stream.js';
+
+const MODIFIED_ARROW = /^\u001b\[1;(\d+)([ABCDHF])$/u;
+const ARROWS: Readonly> = { A: 'up', B: 'down', C: 'right', D: 'left', H: 'home', F: 'end' };
+const NON_ASCII_PRINTABLE = /^[^\x00-\x7f\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]$/u;
+const MODIFIER_BASE = 1;
+const SHIFT_BIT = 1;
+const ALT_BIT = 2;
+const CTRL_BIT = 4;
+
+// Bijou's raw parser handles ASCII, mouse packets and ordinary escape keys.
+// Its onData host-message port carries decoded keys that it cannot express.
+export function workspaceTerminalIO(io: IOPort): IOPort {
+ const subscribers = new Set<(message: KeyMsg) => void>();
+ return {
+ ...io,
+ rawInput(callback) {
+ const stream = terminalInputStream((raw) => {
+ const message = extendedTerminalKey(raw);
+ if (message == null) callback(raw);
+ else for (const subscriber of subscribers) subscriber(message);
+ });
+ const handle = io.rawInput(stream.accept);
+ return { dispose() { stream.dispose(); handle.dispose(); } };
+ },
+ onData(callback) {
+ const subscriber = (message: KeyMsg) => callback(message);
+ subscribers.add(subscriber);
+ const handle = io.onData?.(callback);
+ return { dispose() { subscribers.delete(subscriber); handle?.dispose(); } };
+ },
+ };
+}
+
+function extendedTerminalKey(raw: string): KeyMsg | undefined {
+ if (NON_ASCII_PRINTABLE.test(raw)) return { type: 'key', key: raw, ctrl: false, alt: false, shift: false };
+ const match = MODIFIED_ARROW.exec(raw);
+ if (match == null) return undefined;
+ const bits = Number(match[1]) - MODIFIER_BASE;
+ return {
+ type: 'key', key: ARROWS[match[2] ?? ''] ?? '',
+ shift: (bits & SHIFT_BIT) !== 0, alt: (bits & ALT_BIT) !== 0, ctrl: (bits & CTRL_BIT) !== 0,
+ };
+}
diff --git a/src/app/workspace/drawer.ts b/src/app/workspace/drawer.ts
index f62b5a69..f8553bb4 100644
--- a/src/app/workspace/drawer.ts
+++ b/src/app/workspace/drawer.ts
@@ -46,6 +46,7 @@ function openFileDrawer(
const next = withFocusPane({
...model,
fileDrawerOpen: true,
+ fileSearchEditing: false,
}, FocusPanes.Files);
if (model.fileDrawerOpen) {
@@ -77,7 +78,7 @@ export function closeDrawer(
createDrawerAnimationCmd: CreateDrawerAnimationCmd,
): [WorkspaceModel, Cmd[]] {
const next = kind === DrawerKinds.Files
- ? { ...model, fileDrawerOpen: false }
+ ? { ...model, fileDrawerOpen: false, fileSearchEditing: false, fileHoveredIndex: undefined }
: { ...model, graftDrawerOpen: false };
const focusState: Omit = {
diff --git a/src/app/workspace/file-explorer-preview-view.ts b/src/app/workspace/file-explorer-preview-view.ts
new file mode 100644
index 00000000..89debbd0
--- /dev/null
+++ b/src/app/workspace/file-explorer-preview-view.ts
@@ -0,0 +1,46 @@
+import { createSurface, stringToSurface, type Surface } from '@flyingrobots/bijou';
+import { FilePreviewKinds, type FilePreviewResult } from '../../ports/file-preview.js';
+import { fitBlock } from '../../ui/workspace-render.js';
+import { fileExplorerHasFocus, selectedFileSearchEntry } from './file-search.js';
+import { FileEntryKinds } from '../../ports/file-system.js';
+import type { WorkspaceModel } from './model.js';
+import { applyBackground, fillSurface } from './surface-fill.js';
+
+const PREVIEW_PAD = 1;
+const PAD_MULTIPLIER = 2;
+const PREVIEW_BODY_TOP = 3;
+const PREVIEW_LABEL = 'fileExplorer.preview_label';
+const PREVIEW_LOADING = 'fileExplorer.preview_loading';
+const PREVIEW_BINARY = 'fileExplorer.preview_binary';
+const PREVIEW_UNAVAILABLE = 'fileExplorer.preview_unavailable';
+const PREVIEW_TRUNCATED = 'fileExplorer.preview_truncated';
+const TAB_SPACES = ' ';
+const UNSAFE_CONTROLS = /[\p{Cc}\p{Cf}]/gu;
+
+export function renderFileExplorerPreview(model: WorkspaceModel, width: number, height: number): Surface | undefined {
+ if (!fileExplorerHasFocus(model)) return undefined;
+ const entry = selectedFileSearchEntry(model);
+ if (entry?.kind !== FileEntryKinds.File) return undefined;
+ const surface = createSurface(width, height);
+ fillSurface(surface, model.jeditTheme.surface.workspace);
+ const innerWidth = Math.max(0, width - PREVIEW_PAD * PAD_MULTIPLIER);
+ const header = `${model.i18n.t(PREVIEW_LABEL)} · ${entry.name}`;
+ const label = stringToSurface(fitBlock(header, innerWidth, 1), innerWidth, 1);
+ applyBackground(label, model.jeditTheme.surface.header);
+ surface.blit(label, PREVIEW_PAD, PREVIEW_PAD);
+ const result = model.fileExplorerPreview?.entry === entry ? model.fileExplorerPreview.result : undefined;
+ const lines = previewLines(model, result);
+ const bodyHeight = Math.max(0, height - PREVIEW_BODY_TOP - PREVIEW_PAD);
+ const body = stringToSurface(fitBlock(lines.join('\n'), innerWidth, bodyHeight), innerWidth, bodyHeight);
+ applyBackground(body, model.jeditTheme.surface.workspace);
+ surface.blit(body, PREVIEW_PAD, PREVIEW_BODY_TOP);
+ return surface;
+}
+
+function previewLines(model: WorkspaceModel, result: FilePreviewResult | undefined): readonly string[] {
+ if (result == null) return [model.i18n.t(PREVIEW_LOADING)];
+ if (result.kind === FilePreviewKinds.Binary) return [model.i18n.t(PREVIEW_BINARY)];
+ if (result.kind === FilePreviewKinds.Unavailable) return [model.i18n.t(PREVIEW_UNAVAILABLE)];
+ const lines = result.lines.map((line) => line.replaceAll('\t', TAB_SPACES).replace(UNSAFE_CONTROLS, ''));
+ return result.truncated ? [model.i18n.t(PREVIEW_TRUNCATED), ...lines] : lines;
+}
diff --git a/src/app/workspace/file-explorer-preview.ts b/src/app/workspace/file-explorer-preview.ts
new file mode 100644
index 00000000..9c6c622c
--- /dev/null
+++ b/src/app/workspace/file-explorer-preview.ts
@@ -0,0 +1,78 @@
+import type { Cmd } from '@flyingrobots/bijou-tui';
+import { FilePreviewKinds, FILE_PREVIEW_MAX_LINES, type FilePreviewSource, type FilePreviewResult } from '../../ports/file-preview.js';
+import { FileEntryKinds, type FileEntry } from '../../ports/file-system.js';
+import { fileExplorerHasFocus, selectedFileSearchEntry } from './file-search.js';
+import { findWorkspaceBufferRecordByPath } from './workspace-buffer-registry.js';
+import { isWorkspaceTextAuthorityOpened } from './workspace-text-authority.js';
+import type { WorkspaceTextReadingCache } from './workspace-text-reading-cache.js';
+import { WorkspaceMessageTypes, type WorkspaceMsg } from './msg.js';
+import type { WorkspaceModel } from './model.js';
+
+export interface FileExplorerPreview {
+ readonly requestId: number;
+ readonly entry: FileEntry;
+ readonly result?: FilePreviewResult;
+}
+
+export interface FileExplorerPreviewMessage extends FileExplorerPreview {
+ readonly result: FilePreviewResult;
+}
+
+type Result = [WorkspaceModel, Cmd[]];
+const FIRST_LINE = 0;
+type LoadedFilePreview = Extract;
+
+export function synchronizeFileExplorerPreview(result: Result, source: FilePreviewSource | undefined): Result {
+ const [model, commands] = result;
+ const entry = fileExplorerHasFocus(model) ? selectedFileSearchEntry(model) : undefined;
+ if (entry?.kind !== FileEntryKinds.File) return clearFileExplorerPreview(result);
+ if (model.fileExplorerPreview?.entry === entry || source == null) return result;
+ const requestId = (model.fileExplorerPreviewRequestId ?? 0) + 1;
+ const preview = { requestId, entry, result: bufferPreview(model, entry.path) };
+ const next = { ...model, fileExplorerPreviewRequestId: requestId, fileExplorerPreview: preview };
+ return preview.result != null ? [next, commands] : [next, [...commands, loadPreviewCmd(preview, source)]];
+}
+
+export function applyFileExplorerPreview(model: WorkspaceModel, msg: FileExplorerPreviewMessage): WorkspaceModel {
+ return model.fileExplorerPreview?.requestId === msg.requestId &&
+ model.fileExplorerPreview.entry === msg.entry && fileExplorerHasFocus(model)
+ ? { ...model, fileExplorerPreview: msg }
+ : model;
+}
+
+function clearFileExplorerPreview(result: Result): Result {
+ return result[0].fileExplorerPreview == null
+ ? result
+ : [{ ...result[0], fileExplorerPreview: undefined }, result[1]];
+}
+
+// Open buffers preview their current text-authority reading, not the editor
+// projection, which can be a stale opening window. A reading that does not start
+// at the first line cannot show the file's head, so the file is read instead.
+function bufferPreview(model: WorkspaceModel, path: string): FilePreviewResult | undefined {
+ const authority = isWorkspaceTextAuthorityOpened(model.textAuthority) && model.textAuthority.filePath === path
+ ? model.textAuthority
+ : findWorkspaceBufferRecordByPath(model, path)?.textAuthority;
+ if (authority != null) return readingPreview(authority.cache);
+ return model.editor?.path === path ? linesPreview(model.editor.lines, model.editor.lines.length) : undefined;
+}
+
+function readingPreview(cache: WorkspaceTextReadingCache | undefined): FilePreviewResult | undefined {
+ if (cache == null || cache.startLine !== FIRST_LINE) return undefined;
+ const preview = linesPreview(cache.lines, cache.totalLineCount);
+ return cache.truncated ? { ...preview, truncated: true } : preview;
+}
+
+function linesPreview(lines: readonly string[], totalLineCount: number): LoadedFilePreview {
+ const shown = lines.slice(0, FILE_PREVIEW_MAX_LINES);
+ return { kind: FilePreviewKinds.Loaded, lines: shown, truncated: totalLineCount > shown.length };
+}
+
+function loadPreviewCmd(preview: FileExplorerPreview, source: FilePreviewSource): Cmd {
+ return () => {
+ let result: FilePreviewResult;
+ try { result = source.loadFilePreview(preview.entry.path); }
+ catch { result = { kind: FilePreviewKinds.Unavailable }; }
+ return { type: WorkspaceMessageTypes.FileExplorerPreviewResult, ...preview, result };
+ };
+}
diff --git a/src/app/workspace/file-explorer-view.ts b/src/app/workspace/file-explorer-view.ts
new file mode 100644
index 00000000..5ef41c14
--- /dev/null
+++ b/src/app/workspace/file-explorer-view.ts
@@ -0,0 +1,56 @@
+import { createSurface, stringToSurface, type Surface } from '@flyingrobots/bijou';
+import { fitBlock, formatTreeLine } from '../../ui/workspace-render.js';
+import { highlightFileSearchMatches } from '../../ui/file-search-results.js';
+import { renderFileSearchInput, type FileSearchInputOptions } from '../../ui/file-search-input.js';
+import type { WorkspaceModel } from './model.js';
+import { fileSearchEntries, fileSearchHasFocus, fileSearchState } from './file-search.js';
+import {
+ DRAWER_INNER_PAD, FILE_SEARCH_ROW, FILE_SEARCH_SEPARATOR_ROW, FILE_SEARCH_LIST_TOP,
+ fileSearchListHeight, listScrollOffset,
+} from './viewport.js';
+import { applyBackground, fillSurface } from './surface-fill.js';
+
+const PAD_MULTIPLIER = 2;
+const MIN_WIDTH = 1;
+const SEARCH_LABEL_KEY = 'fileExplorer.search_label';
+const SEARCH_PLACEHOLDER_KEY = 'fileExplorer.placeholder';
+const SEARCH_EMPTY_KEY = 'fileExplorer.empty';
+const INPUT_RULE = '─';
+
+export function fileSearchInputOptions(model: WorkspaceModel, width: number): FileSearchInputOptions {
+ return {
+ state: fileSearchState(model), label: model.i18n.t(SEARCH_LABEL_KEY),
+ placeholder: model.i18n.t(SEARCH_PLACEHOLDER_KEY), focused: fileSearchHasFocus(model),
+ width, theme: model.jeditTheme,
+ };
+}
+
+export function renderFileExplorer(model: WorkspaceModel, width: number, height: number): Surface {
+ const surface = createSurface(width, height);
+ fillSurface(surface, model.jeditTheme.surface.drawer);
+ const innerWidth = Math.max(MIN_WIDTH, width - DRAWER_INNER_PAD * PAD_MULTIPLIER);
+ surface.blit(renderFileSearchInput(fileSearchInputOptions(model, innerWidth)), DRAWER_INNER_PAD, FILE_SEARCH_ROW);
+ const rule = stringToSurface(INPUT_RULE.repeat(innerWidth), innerWidth, 1);
+ applyBackground(rule, model.jeditTheme.surface.drawer);
+ surface.blit(rule, DRAWER_INNER_PAD, FILE_SEARCH_SEPARATOR_ROW);
+ paintFileSearchResults(surface, model, innerWidth, fileSearchListHeight(height));
+ return surface;
+}
+
+function paintFileSearchResults(surface: Surface, model: WorkspaceModel, width: number, height: number): void {
+ if (height <= 0) return;
+ const entries = fileSearchEntries(model);
+ const selected = entries.findIndex(({ index }) => index === model.selectedIndex);
+ const offset = listScrollOffset(selected, entries.length, height);
+ const lines = entries.length === 0
+ ? [model.i18n.t(SEARCH_EMPTY_KEY)]
+ : entries.slice(offset, offset + height).map(({ entry, index }) => formatTreeLine(entry, {
+ selected: index === model.selectedIndex,
+ }));
+ const content = stringToSurface(fitBlock(lines.join('\n'), width, height), width, height);
+ applyBackground(content, model.jeditTheme.surface.drawer);
+ highlightFileSearchMatches(content, entries.slice(offset, offset + height), model.jeditTheme, {
+ selectedIndex: model.selectedIndex, hoveredIndex: model.fileHoveredIndex,
+ });
+ surface.blit(content, DRAWER_INNER_PAD, FILE_SEARCH_LIST_TOP);
+}
diff --git a/src/app/workspace/file-search-input-key.ts b/src/app/workspace/file-search-input-key.ts
new file mode 100644
index 00000000..29a09d33
--- /dev/null
+++ b/src/app/workspace/file-search-input-key.ts
@@ -0,0 +1,91 @@
+import type { Cmd, KeyMsg } from '@flyingrobots/bijou-tui';
+import { fileSearchHasFocus, fileExplorerHasFocus, fileSearchState, fileSearchWithInput, EMPTY_FILE_SEARCH, type FileSearchState } from './file-search.js';
+import type { WorkspaceModel } from './model.js';
+import type { WorkspaceMsg } from './msg.js';
+
+const KEYS = Object.freeze({
+ Left: 'left', Right: 'right', Home: 'home', End: 'end', Backspace: 'backspace', Delete: 'delete',
+ Space: 'space', Clear: 'u', Filter: '/', Escape: 'escape', Enter: 'enter',
+});
+const LITERAL_SPACE = ' ';
+const SINGLE_KEY_LENGTH = 1;
+const NON_PRINTABLE = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u;
+const FORWARD_STEP = 1;
+const BACKWARD_STEP = -1;
+type Edit = (state: FileSearchState) => FileSearchState;
+const EDITS = new Map([
+ [KEYS.Left, (state) => moveCursor(state, BACKWARD_STEP)],
+ [KEYS.Right, (state) => moveCursor(state, FORWARD_STEP)],
+ [KEYS.Home, (state) => ({ ...state, cursorIndex: 0 })],
+ [KEYS.End, (state) => ({ ...state, cursorIndex: Array.from(state.input).length })],
+ [KEYS.Backspace, deleteBeforeCursor],
+ [KEYS.Delete, deleteAtCursor],
+]);
+
+export function updateFileSearchInputKey(
+ msg: KeyMsg,
+ model: WorkspaceModel,
+): [WorkspaceModel, Cmd[]] | undefined {
+ if (!fileExplorerHasFocus(model)) return undefined;
+ const mode = updateFilterMode(msg, model);
+ if (mode != null) return mode;
+ if (!fileSearchHasFocus(model)) return undefined;
+ const state = fileSearchState(model);
+ const next = editFromKey(msg, state);
+ if (next == null) return undefined;
+ if (next.input !== state.input) return [fileSearchWithInput(model, next), []];
+ return [next.cursorIndex === state.cursorIndex ? model : { ...model, fileSearch: next }, []];
+}
+
+function editFromKey(msg: KeyMsg, state: FileSearchState): FileSearchState | undefined {
+ if (isClearKey(msg)) return EMPTY_FILE_SEARCH;
+ if (msg.ctrl || msg.alt) return undefined;
+ const edit = EDITS.get(msg.key);
+ if (edit != null) return edit(state);
+ const input = printableInput(msg);
+ return input == null ? undefined : insertAtCursor(state, input);
+}
+
+function isClearKey(msg: KeyMsg): boolean {
+ return msg.ctrl === true && !msg.alt && msg.key === KEYS.Clear;
+}
+
+function printableInput(msg: KeyMsg): string | undefined {
+ const value = msg.key === KEYS.Space ? LITERAL_SPACE : msg.key;
+ return Array.from(value).length === SINGLE_KEY_LENGTH && !NON_PRINTABLE.test(value)
+ ? (msg.shift ? value.toUpperCase() : value)
+ : undefined;
+}
+
+function moveCursor(state: FileSearchState, delta: number): FileSearchState {
+ return { ...state, cursorIndex: Math.max(0, Math.min(Array.from(state.input).length, state.cursorIndex + delta)) };
+}
+
+function insertAtCursor(state: FileSearchState, input: string): FileSearchState {
+ const characters = Array.from(state.input);
+ characters.splice(state.cursorIndex, 0, ...Array.from(input));
+ return { input: characters.join(''), cursorIndex: state.cursorIndex + Array.from(input).length };
+}
+
+function deleteBeforeCursor(state: FileSearchState): FileSearchState {
+ if (state.cursorIndex <= 0) return state;
+ const characters = Array.from(state.input);
+ characters.splice(state.cursorIndex - FORWARD_STEP, FORWARD_STEP);
+ return { input: characters.join(''), cursorIndex: state.cursorIndex - FORWARD_STEP };
+}
+
+function deleteAtCursor(state: FileSearchState): FileSearchState {
+ const characters = Array.from(state.input);
+ characters.splice(state.cursorIndex, FORWARD_STEP);
+ return { input: characters.join(''), cursorIndex: state.cursorIndex };
+}
+
+function updateFilterMode(msg: KeyMsg, model: WorkspaceModel): [WorkspaceModel, Cmd[]] | undefined {
+ if (msg.ctrl || msg.alt) return undefined;
+ if (!model.fileSearchEditing) {
+ return msg.key === KEYS.Filter ? [{ ...model, fileSearchEditing: true }, []] : undefined;
+ }
+ return msg.key === KEYS.Escape || msg.key === KEYS.Enter
+ ? [{ ...model, fileSearchEditing: false }, []]
+ : undefined;
+}
diff --git a/src/app/workspace/file-search.ts b/src/app/workspace/file-search.ts
new file mode 100644
index 00000000..148db04d
--- /dev/null
+++ b/src/app/workspace/file-search.ts
@@ -0,0 +1,96 @@
+import type { FileEntry } from '../../ports/file-system.js';
+import { FileEntryKinds } from '../../ports/file-system.js';
+import { FocusPanes } from '../../ui/panel-focus.js';
+import { scrollIndexByRows } from '../../ui/mouse-scroll.js';
+import { wrapIndex } from '../list-index.js';
+import type { WorkspaceModel } from './model.js';
+
+export interface FileSearchState {
+ readonly input: string;
+ readonly cursorIndex: number;
+}
+
+export interface FileSearchEntry {
+ readonly entry: FileEntry;
+ readonly index: number;
+ readonly matchIndexes: readonly number[];
+}
+
+export const EMPTY_FILE_SEARCH: FileSearchState = Object.freeze({ input: '', cursorIndex: 0 });
+const PATH_SEPARATOR = '/';
+const NO_SELECTION = -1;
+
+export function fileSearchState(model: Pick): FileSearchState {
+ return model.fileSearch ?? EMPTY_FILE_SEARCH;
+}
+
+export function fileExplorerHasFocus(model: WorkspaceModel): boolean {
+ return model.fileDrawerOpen && model.focusPane === FocusPanes.Files &&
+ !model.settingsOpen && !model.quitConfirmOpen && !model.startupFileModalOpen && !model.commandLine?.active;
+}
+
+export function fileSearchHasFocus(model: WorkspaceModel): boolean {
+ return fileExplorerHasFocus(model) && model.fileSearchEditing === true;
+}
+
+export function fileSearchEntries(model: Pick): readonly FileSearchEntry[] {
+ const query = fileSearchState(model).input.toLowerCase();
+ return model.entries.flatMap((entry, index) => {
+ if (query.length === 0) return [{ entry, index, matchIndexes: [] }];
+ if (entry.kind === FileEntryKinds.Parent) return [];
+ const path = fuzzyPathMatches(entry.name, query) != null ? entry.name : relativeFilePath(model.workspaceSearchRoot ?? model.workspaceRoot, entry);
+ const matches = fuzzyPathMatches(path, query);
+ if (matches == null) return [];
+ const nameStart = Array.from(path).length - Array.from(entry.name).length;
+ return [{ entry, index, matchIndexes: matches.filter((offset) => offset >= nameStart).map((offset) => offset - nameStart) }];
+ });
+}
+
+function relativeFilePath(root: string, entry: FileEntry): string {
+ const prefix = root?.endsWith(PATH_SEPARATOR) ? root : `${root ?? ''}${PATH_SEPARATOR}`;
+ const path = entry.searchPath ?? entry.path ?? entry.name;
+ // Outside the search root (after following `..` upward) only the name is
+ // searchable; the absolute path would leak ancestor text into every match.
+ return path.startsWith(prefix) ? path.slice(prefix.length) : entry.name;
+}
+
+function fuzzyPathMatches(path: string, query: string): readonly number[] | undefined {
+ const candidate = Array.from(path).flatMap((character, index) => (
+ Array.from(character.toLowerCase()).map((value) => ({ value, index }))
+ ));
+ const matches: number[] = [];
+ let offset = 0;
+ for (const character of query) {
+ while (offset < candidate.length && candidate[offset]?.value !== character) offset += 1;
+ const matched = candidate[offset];
+ if (matched == null) return undefined;
+ matches.push(matched.index);
+ offset += 1;
+ }
+ return matches;
+}
+
+export function selectedFileSearchEntry(model: WorkspaceModel): FileEntry | undefined {
+ return fileSearchEntries(model).find(({ index }) => index === model.selectedIndex)?.entry;
+}
+
+export function fileSearchWithInput(model: WorkspaceModel, search: FileSearchState): WorkspaceModel {
+ const next = { ...model, fileSearch: search, fileHoveredIndex: undefined };
+ return { ...next, selectedIndex: fileSearchEntries(next)[0]?.index ?? NO_SELECTION };
+}
+
+export function moveFileSearchSelection(model: WorkspaceModel, delta: number): WorkspaceModel {
+ const entries = fileSearchEntries(model);
+ if (entries.length === 0) return model;
+ const position = entries.findIndex(({ index }) => index === model.selectedIndex);
+ const next = entries[wrapIndex(position + delta, entries.length)];
+ return next == null ? model : { ...model, selectedIndex: next.index, fileHoveredIndex: undefined };
+}
+
+export function scrollFileSearchSelection(model: WorkspaceModel, delta: number): WorkspaceModel {
+ const entries = fileSearchEntries(model);
+ if (entries.length === 0) return model;
+ const position = entries.findIndex(({ index }) => index === model.selectedIndex);
+ const next = entries[scrollIndexByRows(position, entries.length, delta)];
+ return next == null ? model : { ...model, selectedIndex: next.index, fileHoveredIndex: undefined };
+}
diff --git a/src/app/workspace/file-tree.ts b/src/app/workspace/file-tree.ts
index bb193929..139df40d 100644
--- a/src/app/workspace/file-tree.ts
+++ b/src/app/workspace/file-tree.ts
@@ -10,7 +10,6 @@ import type { EditorFilePort } from '../../ports/editor-file.js';
import type { GraftSessionPort } from '../../ports/graft-session.js';
import type { SourceHighlighter } from '../../ports/source-highlighter.js';
import { createNotificationTickCmd, pushErrorToast } from '../../ui/feedback.js';
-import { wrapIndex } from './viewport.js';
import { withFocusPane } from './focus.js';
import { isWorkspaceMarkdownFile } from './editor-session.js';
import { ViewModes } from './view-mode.js';
@@ -20,7 +19,9 @@ import { WorkspaceMessageTypes, type WorkspaceMsg } from './msg.js';
import type { ProductionTextSession } from './production-text-session.js';
import { createWorkspaceTextOpenCmd, defaultWorkspaceTextAperture } from './workspace-text-commands.js';
import { pendingWorkspaceTextOpen } from './workspace-text-authority.js';
+import { EMPTY_FILE_SEARCH, fileSearchState, fileSearchWithInput, moveFileSearchSelection, selectedFileSearchEntry } from './file-search.js';
import {
+ WorkspaceKeys,
isWorkspaceBackKey,
isWorkspaceDownKey,
isWorkspaceOpenKey,
@@ -35,6 +36,8 @@ import {
} from './workspace-buffer-registry.js';
export const FILE_TREE_META_MIN = 0;
+const FILE_TREE_FORWARD_STEP = 1;
+const FILE_TREE_BACKWARD_STEP = -1;
export interface UpdateTreeFromKeyDeps {
readonly fileSystem: FileSystemPort;
@@ -50,6 +53,8 @@ export function updateTreeFromKey(
nowMs: () => number,
deps: UpdateTreeFromKeyDeps,
): [WorkspaceModel, Cmd[]] {
+ const modified = updateModifiedTreeKey(msg, model, nowMs, deps.fileSystem);
+ if (modified != null) return modified;
const navigation = updateTreeNavigationFromKey(msg, model);
if (navigation != null) {
return [navigation, []];
@@ -66,18 +71,29 @@ export function updateTreeFromKey(
: [model, []];
}
+function updateModifiedTreeKey(
+ msg: KeyMsg,
+ model: WorkspaceModel,
+ nowMs: () => number,
+ fileSystem: FileSystemPort,
+): [WorkspaceModel, Cmd[]] | undefined {
+ if (msg.alt && msg.key === WorkspaceKeys.ArrowUp) {
+ return openParentDirectory(model, nowMs, fileSystem);
+ }
+ if (msg.ctrl || msg.alt) {
+ return msg.ctrl && msg.key === WorkspaceKeys.R
+ ? changeDirectory(model, model.cwd, DIRECTORY_ACTION_REFRESH, nowMs, fileSystem)
+ : [model, []];
+ }
+ return undefined;
+}
+
function updateTreeNavigationFromKey(msg: KeyMsg, model: WorkspaceModel): WorkspaceModel | undefined {
if (isWorkspaceDownKey(msg)) {
- return {
- ...model,
- selectedIndex: wrapIndex(model.selectedIndex + 1, model.entries.length),
- };
+ return moveFileSearchSelection(model, FILE_TREE_FORWARD_STEP);
}
if (isWorkspaceUpKey(msg)) {
- return {
- ...model,
- selectedIndex: wrapIndex(model.selectedIndex - 1, model.entries.length),
- };
+ return moveFileSearchSelection(model, FILE_TREE_BACKWARD_STEP);
}
return undefined;
}
@@ -98,7 +114,7 @@ function openSelectedTreeEntry(
nowMs: () => number,
deps: UpdateTreeFromKeyDeps,
): [WorkspaceModel, Cmd[]] {
- const entry = model.entries[model.selectedIndex];
+ const entry = selectedFileSearchEntry(model);
if (entry == null) {
return [model, []];
}
@@ -152,12 +168,12 @@ function openDirectory(
cwd: string,
fileSystem: FileSystemPort,
): WorkspaceModel {
- return {
+ return fileSearchWithInput({
...model,
cwd,
entries: fileSystem.loadEntries(cwd),
selectedIndex: 0,
- };
+ }, cwd === model.cwd ? fileSearchState(model) : EMPTY_FILE_SEARCH);
}
function changeDirectory(
diff --git a/src/app/workspace/global-key-bindings.ts b/src/app/workspace/global-key-bindings.ts
index 2de245f0..98bffbf8 100644
--- a/src/app/workspace/global-key-bindings.ts
+++ b/src/app/workspace/global-key-bindings.ts
@@ -91,7 +91,7 @@ function updateFocusKey(msg: KeyMsg, model: WorkspaceModel): KeyBindingResult |
if (msg.key !== WorkspaceKeys.Tab || !hasFocusablePeers(focusState)) {
return undefined;
}
- return [{ ...model, focusPane: cycleFocusPane(focusState) }, []];
+ return [{ ...model, focusPane: cycleFocusPane(focusState), fileSearchEditing: false }, []];
}
function updateDrawerKey(
diff --git a/src/app/workspace/init.ts b/src/app/workspace/init.ts
index 51a13a6d..a7fd4c33 100644
--- a/src/app/workspace/init.ts
+++ b/src/app/workspace/init.ts
@@ -9,6 +9,7 @@ import { ViewModes } from "./view-mode.js";
import { TEXT_RUNTIME_PROFILE_ECHO_HOSTED } from "../text-runtime-profile.js";
import { createWorkspaceTextAuthority } from "./workspace-text-authority.js";
import { emptyWorkspaceBufferRegistry } from "./workspace-buffer-registry.js";
+import { EMPTY_FILE_SEARCH } from './file-search.js';
import {
initialStartupFileModalState,
INITIAL_WORKSPACE_CAUSAL_GUTTER_BASIS,
@@ -26,6 +27,7 @@ export interface WorkspaceInitialModelSnapshot {
readonly i18n: I18nPort;
readonly entries: readonly FileEntry[];
readonly nowMs: number;
+ readonly searchRoot?: string;
}
export function createInitialModel(
@@ -39,9 +41,13 @@ export function createInitialModel(
return {
i18n,
workspaceRoot: cwd,
+ workspaceSearchRoot: snapshot.searchRoot,
cwd,
entries,
selectedIndex: 0,
+ fileSearch: EMPTY_FILE_SEARCH,
+ fileSearchEditing: false,
+ fileExplorerPreviewRequestId: 0,
editor: undefined,
...initialBufferRegistryState(),
textRuntimeProfile,
diff --git a/src/app/workspace/key-bindings.ts b/src/app/workspace/key-bindings.ts
index 7644d04b..a5440d67 100644
--- a/src/app/workspace/key-bindings.ts
+++ b/src/app/workspace/key-bindings.ts
@@ -13,6 +13,7 @@ import { clearWorkspaceInlinePanelAfterKey } from './workspace-inline-panel.js';
import { updateSettingsKey } from './settings-key-bindings.js';
import { updateStartupFileModalKey } from './startup-file-modal-key-bindings.js';
import { updateTitleScreenKey } from './title-screen-key-bindings.js';
+import { updateFileSearchInputKey } from './file-search-input-key.js';
export type { UpdateFromKeyDeps } from './key-binding-context.js';
@@ -24,6 +25,7 @@ export function updateFromKey(
context: WorkspaceKeyBindingContext,
): KeyBindingResult {
const result = updateQuitConfirmationKey(msg, model)
+ ?? updateFileSearchInputKey(msg, model)
?? updateHardGlobalWorkspaceKey(msg, model)
?? updateWorkspaceOverlayKey(msg, model, context)
?? updateGlobalWorkspaceKey(msg, model, context)
diff --git a/src/app/workspace/model.ts b/src/app/workspace/model.ts
index 618bf281..43deca39 100644
--- a/src/app/workspace/model.ts
+++ b/src/app/workspace/model.ts
@@ -21,15 +21,23 @@ import type {
} from "./command-completion-preview.js";
import type { WorkspaceInlinePanel } from "./workspace-inline-panel.js";
import type { WorkspaceCausalGutterBasis } from "./workspace-causal-gutter-basis.js";
+import type { FileExplorerPreview } from './file-explorer-preview.js';
+import type { FileSearchState } from './file-search.js';
export interface WorkspaceModel
extends StartupFileModalState,
WorkspaceCommandLineFilePreviewState {
readonly i18n: I18nPort;
readonly workspaceRoot: string;
+ readonly workspaceSearchRoot?: string;
readonly cwd: string;
readonly entries: readonly FileEntry[];
readonly selectedIndex: number;
+ readonly fileSearch: FileSearchState;
+ readonly fileSearchEditing: boolean;
+ readonly fileHoveredIndex?: number;
+ readonly fileExplorerPreview?: FileExplorerPreview;
+ readonly fileExplorerPreviewRequestId: number;
readonly editor?: EditorState;
readonly buffers: WorkspaceBufferRegistry;
readonly activeBufferId?: string;
diff --git a/src/app/workspace/mouse.ts b/src/app/workspace/mouse.ts
index 3c0e507d..06276881 100644
--- a/src/app/workspace/mouse.ts
+++ b/src/app/workspace/mouse.ts
@@ -17,35 +17,33 @@ import {
listScrollOffset,
workspaceBodyHeight,
WORKSPACE_BODY_TOP_OFFSET,
+ FILE_SEARCH_ROW,
+ FILE_SEARCH_LIST_TOP,
+ fileSearchListHeight,
} from './viewport.js';
import { FocusPanes } from '../../ui/panel-focus.js';
import { beginWorkspaceSourceHighlightRefresh } from './workspace-source-highlight.js';
+import { fileSearchEntries, fileSearchState, scrollFileSearchSelection } from './file-search.js';
+import { fileSearchCursorAtColumn } from '../../ui/file-search-input.js';
+import { fileSearchInputOptions } from './file-explorer-view.js';
const DRAWER_PAD_MULTIPLIER = 2;
const MOUSE_PRESS = 'press';
+const MOUSE_MOVE = 'move';
const MOUSE_BUTTON_LEFT = 'left';
-// Opening on click needs the filesystem and Echo dependencies the pointer path
-// does not otherwise carry. Injecting a named opener keeps that dependency
-// explicit and lets the selection-only behaviour stand on its own when the
-// caller has nothing to open with.
-export interface WorkspaceEntryOpener {
- readonly nowMs: () => number;
- readonly openEntry: (
- model: WorkspaceModel,
- entry: NonNullable,
- ) => [WorkspaceModel, Cmd[]];
-}
-
export function updateFromMouse(
msg: MouseMsg,
model: WorkspaceModel,
sourceHighlighter: SourceHighlighter,
- opener?: WorkspaceEntryOpener,
): [WorkspaceModel, Cmd[]] {
+ const hover = updateFileDrawerHover(msg, model);
+ if (hover != null) return [hover, []];
+ const input = updateFileSearchFromClick(msg, model);
+ if (input != null) return [input, []];
const clicked = updateFileDrawerFromClick(msg, model);
if (clicked != null) {
- return openClickedEntry(clicked, opener);
+ return [clicked, []];
}
const deltaRows = mouseScrollDeltaRows(msg);
if (deltaRows === 0) {
@@ -58,10 +56,10 @@ export function updateFromMouse(
return updateEditorFromMouse(model, deltaRows, sourceHighlighter);
}
-// Row and column map onto the list the renderer drew: the drawer is blitted at
+// Row and column map onto the pinned list the renderer drew: the drawer is blitted at
// (layout.fileDrawer.x, WORKSPACE_BODY_TOP_OFFSET) and its content is inset by
-// DRAWER_INNER_PAD. Entry index equals line index because the list does not
-// scroll -- long directories are truncated rather than paged today.
+// DRAWER_INNER_PAD. Filtered positions map through the scroll offset back to
+// original entry indexes.
function updateFileDrawerFromClick(
msg: MouseMsg,
model: WorkspaceModel,
@@ -72,14 +70,14 @@ function updateFileDrawerFromClick(
const index = fileDrawerEntryIndexAt(msg, model);
return index == null
? undefined
- : { ...model, focusPane: FocusPanes.Files, selectedIndex: index };
+ : { ...model, focusPane: FocusPanes.Files, selectedIndex: index, fileSearchEditing: false };
}
function fileDrawerEntryIndexAt(
msg: MouseMsg,
model: WorkspaceModel,
): number | undefined {
- if (!model.fileDrawerOpen) {
+ if (!fileDrawerPointerEnabled(model)) {
return undefined;
}
const drawer = resolveWorkspaceLayout(
@@ -93,31 +91,28 @@ function fileDrawerEntryIndexAt(
if (!withinDrawer) {
return undefined;
}
- const row = msg.row - WORKSPACE_BODY_TOP_OFFSET - DRAWER_INNER_PAD;
- const listHeight = workspaceBodyHeight({
+ const row = msg.row - WORKSPACE_BODY_TOP_OFFSET - FILE_SEARCH_LIST_TOP;
+ const listHeight = fileSearchListHeight(workspaceBodyHeight({
rows: model.rows,
footerVisible: model.footerVisible,
- }) - (DRAWER_INNER_PAD * DRAWER_PAD_MULTIPLIER);
+ }));
if (row < 0 || row >= listHeight) {
return undefined;
}
+ const entries = fileSearchEntries(model);
+ const position = entries.findIndex(({ index }) => index === model.selectedIndex);
const index = row + listScrollOffset(
- model.selectedIndex,
- model.entries.length,
+ position,
+ entries.length,
listHeight,
);
- return index < model.entries.length ? index : undefined;
+ return entries[index]?.index;
}
-function openClickedEntry(
- model: WorkspaceModel,
- opener: WorkspaceEntryOpener | undefined,
-): [WorkspaceModel, Cmd[]] {
- const entry = model.entries[model.selectedIndex];
- if (opener == null || entry == null) {
- return [model, []];
- }
- return opener.openEntry(model, entry);
+function updateFileDrawerHover(msg: MouseMsg, model: WorkspaceModel): WorkspaceModel | undefined {
+ if (msg.action !== MOUSE_MOVE) return undefined;
+ const index = fileDrawerEntryIndexAt(msg, model);
+ return model.fileHoveredIndex === index ? model : { ...model, fileHoveredIndex: index };
}
function updateScrollableDrawerFromMouse(model: WorkspaceModel, deltaRows: number): WorkspaceModel | undefined {
@@ -128,7 +123,7 @@ function updateScrollableDrawerFromMouse(model: WorkspaceModel, deltaRows: numbe
};
}
if (model.focusPane === FocusPanes.Files && model.fileDrawerOpen) {
- return { ...model, selectedIndex: scrollIndexByRows(model.selectedIndex, model.entries.length, deltaRows) };
+ return scrollFileSearchSelection(model, deltaRows);
}
if (model.focusPane === FocusPanes.Graft && model.graftDrawerOpen) {
return {
@@ -139,6 +134,27 @@ function updateScrollableDrawerFromMouse(model: WorkspaceModel, deltaRows: numbe
return undefined;
}
+function fileDrawerPointerEnabled(model: WorkspaceModel): boolean {
+ return model.fileDrawerOpen && !model.settingsOpen && !model.quitConfirmOpen &&
+ !model.startupFileModalOpen && !model.commandLine?.active;
+}
+
+function updateFileSearchFromClick(msg: MouseMsg, model: WorkspaceModel): WorkspaceModel | undefined {
+ if (msg.action !== MOUSE_PRESS || msg.button !== MOUSE_BUTTON_LEFT || !fileDrawerPointerEnabled(model)) return undefined;
+ if (msg.row !== WORKSPACE_BODY_TOP_OFFSET + FILE_SEARCH_ROW) return undefined;
+ const layout = resolveWorkspaceLayout(model.columns, model.fileDrawerProgress, model.graftDrawerProgress).fileDrawer;
+ const innerWidth = layout.width - DRAWER_INNER_PAD * DRAWER_PAD_MULTIPLIER;
+ const column = msg.col - layout.x - DRAWER_INNER_PAD;
+ if (column < 0 || column >= innerWidth) return undefined;
+ return {
+ ...model, focusPane: FocusPanes.Files, fileSearchEditing: true,
+ fileSearch: {
+ ...fileSearchState(model),
+ cursorIndex: fileSearchCursorAtColumn(fileSearchInputOptions(model, innerWidth), column),
+ },
+ };
+}
+
function updateEditorFromMouse(
model: WorkspaceModel,
deltaRows: number,
diff --git a/src/app/workspace/msg.ts b/src/app/workspace/msg.ts
index 66f5b733..7e615f76 100644
--- a/src/app/workspace/msg.ts
+++ b/src/app/workspace/msg.ts
@@ -1,3 +1,4 @@
+import type { FileExplorerPreviewMessage } from './file-explorer-preview.js';
import type { RuntimeIssue } from "@flyingrobots/bijou-tui";
import type { GraftDiagnosticsReport } from "../../ports/graft-diagnostics.js";
import type { GraftInfo } from "../../ports/graft-session.js";
@@ -50,6 +51,7 @@ const WORKSPACE_INPUT_MESSAGE_KEY = "key";
const WORKSPACE_INPUT_MESSAGE_MOUSE = "mouse";
export const WorkspaceMessageTypes = Object.freeze({
+ FileExplorerPreviewResult: 'file-explorer-preview-result',
DrawerProgress: WORKSPACE_MESSAGE_DRAWER_PROGRESS,
GraftDiagnostics: WORKSPACE_MESSAGE_GRAFT_DIAGNOSTICS,
GraftInfo: WORKSPACE_MESSAGE_GRAFT_INFO,
@@ -96,6 +98,7 @@ export type WorkspaceWhyRangeOutcome =
};
export type WorkspaceMsg =
+ | ({ readonly type: typeof WorkspaceMessageTypes.FileExplorerPreviewResult } & FileExplorerPreviewMessage)
| {
type: typeof WorkspaceMessageTypes.DrawerProgress;
kind: DrawerKind;
diff --git a/src/app/workspace/runtime.ts b/src/app/workspace/runtime.ts
index de5defe0..563a9a47 100644
--- a/src/app/workspace/runtime.ts
+++ b/src/app/workspace/runtime.ts
@@ -1,3 +1,4 @@
+import { applyFileExplorerPreview, synchronizeFileExplorerPreview } from './file-explorer-preview.js';
import { createInitialModel, workspaceAnimationIsActive } from "./init.js";
import type { WorkspaceModel } from "./model.js";
import {
@@ -84,9 +85,9 @@ function updateWorkspaceRuntime(
msg: WorkspaceRuntimeMsg,
model: WorkspaceModel,
): WorkspaceRuntimeResult {
- return syncWorkspaceRuntimeResult(
+ return synchronizeFileExplorerPreview(syncWorkspaceRuntimeResult(
rebaseFrameClock(deps, model, updateWorkspaceRuntimeState(deps, msg, model)),
- );
+ ), deps.filePreviewSource);
}
// While the workspace is idle the tick handler returns the model untouched, so
@@ -199,6 +200,7 @@ function updateWorkspaceStateMessage(
msg: WorkspaceRuntimeMsg,
model: WorkspaceModel,
): WorkspaceRuntimeResult | undefined {
+ if (msg.type === WorkspaceMessageTypes.FileExplorerPreviewResult) return [applyFileExplorerPreview(model, msg), []];
const animation = updateAnimationStateMessage(msg, model);
if (animation != null) {
return animation;
@@ -400,9 +402,6 @@ function updateWorkspaceInputMessage(
msg,
model,
deps.sourceHighlighter,
- deps.openEntry == null
- ? undefined
- : { nowMs: deps.nowMs, openEntry: deps.openEntry },
);
}
return msg.type === WorkspaceInputMessageTypes.Key
diff --git a/src/app/workspace/settings-key-bindings.ts b/src/app/workspace/settings-key-bindings.ts
index b6cdb59c..c6144fd1 100644
--- a/src/app/workspace/settings-key-bindings.ts
+++ b/src/app/workspace/settings-key-bindings.ts
@@ -35,6 +35,7 @@ export function updateSettingsKey(
return [{
...model,
settingsOpen: !model.settingsOpen,
+ fileSearchEditing: false,
settingsFocusIndex: 0,
settingsDiagnosticsOpen: false,
}, []];
diff --git a/src/app/workspace/viewer-content.ts b/src/app/workspace/viewer-content.ts
index a02d4450..3369e689 100644
--- a/src/app/workspace/viewer-content.ts
+++ b/src/app/workspace/viewer-content.ts
@@ -1,3 +1,4 @@
+import { renderFileExplorerPreview } from './file-explorer-preview-view.js';
import { createSurface, type Surface } from '@flyingrobots/bijou';
import { paintMarkdownPreview } from '../../ui/markdown-preview.js';
import { renderSourceViewer } from '../../ui/source-viewer.js';
@@ -27,6 +28,8 @@ export function createViewerContentRenderer(): ViewerContentRenderer {
}
export function renderViewer(model: WorkspaceModel, width: number, height: number): Surface {
+ const explorerPreview = renderFileExplorerPreview(model, width, height);
+ if (explorerPreview != null) return explorerPreview;
const editor = displayEditorForWorkspaceModel(model);
if (editor == null) {
return renderJimLogoScreen(width, height, model.jeditTheme);
diff --git a/src/app/workspace/viewer-drawers.ts b/src/app/workspace/viewer-drawers.ts
index 5483dc56..57503612 100644
--- a/src/app/workspace/viewer-drawers.ts
+++ b/src/app/workspace/viewer-drawers.ts
@@ -3,11 +3,11 @@ import { DrawerKinds, type DrawerKind } from '../../ui/drawer-layout.js';
import { renderGraftDrawerLines } from '../../ui/graft-drawer.js';
import {
fitBlock,
- formatTreeLine,
} from '../../ui/workspace-render.js';
import type { WorkspaceModel } from './model.js';
-import { DRAWER_INNER_PAD, listScrollOffset } from './viewport.js';
+import { DRAWER_INNER_PAD } from './viewport.js';
import { applyBackground, fillSurface } from './surface-fill.js';
+import { renderFileExplorer } from './file-explorer-view.js';
const MIN_VIEWPORT_DIMENSION = 1;
const DRAWER_PAD_MULTIPLIER = 2;
@@ -16,26 +16,7 @@ export function renderDrawer(kind: DrawerKind, model: WorkspaceModel, width: num
if (kind === DrawerKinds.Graft) {
return renderGraftDrawer(model, width, height);
}
- const surface = createSurface(width, height);
- fillSurface(surface, model.jeditTheme.surface.drawer);
-
- const listWidth = Math.max(MIN_VIEWPORT_DIMENSION, width - (DRAWER_INNER_PAD * DRAWER_PAD_MULTIPLIER));
- const listHeight = Math.max(MIN_VIEWPORT_DIMENSION, height - (DRAWER_INNER_PAD * DRAWER_PAD_MULTIPLIER));
- const offset = listScrollOffset(
- model.selectedIndex,
- model.entries.length,
- listHeight,
- );
- const lines = model.entries
- .slice(offset, offset + listHeight)
- .map((entry, index) => formatTreeLine(entry, {
- selected: index + offset === model.selectedIndex,
- }));
- const content = stringToSurface(fitBlock(lines.join('\n'), listWidth, listHeight), listWidth, listHeight);
- applyBackground(content, model.jeditTheme.surface.drawer);
- surface.blit(content, DRAWER_INNER_PAD, DRAWER_INNER_PAD);
-
- return surface;
+ return renderFileExplorer(model, width, height);
}
function renderGraftDrawer(model: WorkspaceModel, width: number, height: number): Surface {
diff --git a/src/app/workspace/viewer.ts b/src/app/workspace/viewer.ts
index 81b8cc9e..f372bf9b 100644
--- a/src/app/workspace/viewer.ts
+++ b/src/app/workspace/viewer.ts
@@ -132,6 +132,7 @@ function paintWorkspaceFooter(screen: Surface, model: WorkspaceModel): void {
i18n: model.i18n,
focusPane: model.focusPane,
fileDrawerOpen: model.fileDrawerOpen,
+ fileSearchEditing: model.fileSearchEditing,
graftDrawerOpen: model.graftDrawerOpen,
viewMode: model.viewMode,
markdownPreviewActive: isWorkspaceMarkdownPreviewAvailable(model),
diff --git a/src/app/workspace/viewport.ts b/src/app/workspace/viewport.ts
index b69fc0ea..892b61a0 100644
--- a/src/app/workspace/viewport.ts
+++ b/src/app/workspace/viewport.ts
@@ -8,6 +8,13 @@ export const MIN_ROWS = 12;
export const VIEWER_LEFT_PAD = 4;
export const VIEWER_TOP_PAD = 1;
export const DRAWER_INNER_PAD = 1;
+export const FILE_SEARCH_ROW = DRAWER_INNER_PAD;
+export const FILE_SEARCH_SEPARATOR_ROW = FILE_SEARCH_ROW + 1;
+export const FILE_SEARCH_LIST_TOP = FILE_SEARCH_SEPARATOR_ROW + 1;
+
+export function fileSearchListHeight(height: number): number {
+ return Math.max(0, height - FILE_SEARCH_LIST_TOP - DRAWER_INNER_PAD);
+}
// Rows above the workspace body: the title row and its rule. Shared so pointer
// hit-testing and the renderer cannot disagree about where the body starts.
export const WORKSPACE_BODY_TOP_OFFSET = 2;
diff --git a/src/app/workspace/workspace-runtime-dependencies.ts b/src/app/workspace/workspace-runtime-dependencies.ts
index 13a62c31..be362fb4 100644
--- a/src/app/workspace/workspace-runtime-dependencies.ts
+++ b/src/app/workspace/workspace-runtime-dependencies.ts
@@ -1,3 +1,4 @@
+import type { FilePreviewSource } from '../../ports/file-preview.js';
import type { Cmd, KeyMsg, MouseMsg, ResizeMsg, RuntimeIssue } from '@flyingrobots/bijou-tui';
import type { DrawerKind } from '../../ui/drawer-layout.js';
import type { EditorFilePort } from '../../ports/editor-file.js';
@@ -12,17 +13,13 @@ import type { WorkspaceMsg } from './msg.js';
import type { ProductionTextSession } from './production-text-session.js';
import type { ProfilerTracePort } from '../raytracer-profiler.js';
import type { renderWorkspace } from './viewer.js';
-import type { WorkspaceEntryOpener } from './mouse.js';
export type WorkspaceRuntimeMsg = WorkspaceMsg | ResizeMsg | KeyMsg | MouseMsg;
export type WorkspaceRuntimeResult = [WorkspaceModel, Cmd[]];
export type WorkspaceResizeMsg = ResizeMsg;
export interface WorkspaceRuntimeDependencies {
- // Supplied by the composition root so pointer clicks can open an entry.
- // Optional: the pointer path still selects without it, and test harnesses
- // that never click do not have to construct the filesystem dependencies.
- readonly openEntry?: WorkspaceEntryOpener['openEntry'];
+ readonly filePreviewSource?: FilePreviewSource;
readonly initialColumns: number;
readonly initialRows: number;
readonly initialWorkingDirectory: string;
diff --git a/src/generated/i18n.ts b/src/generated/i18n.ts
index 5cbbfc1b..b5b8fbec 100644
--- a/src/generated/i18n.ts
+++ b/src/generated/i18n.ts
@@ -78,6 +78,9 @@ export type TranslationSchema = {
readonly "y_end": string;
readonly "gg_top": string;
readonly "esc_cancel": string;
+ readonly "type_filter": string;
+ readonly "arrows_select": string;
+ readonly "slash_filter": string;
};
readonly "evidence": {
readonly "head": string;
@@ -167,6 +170,16 @@ export type TranslationSchema = {
readonly "why": {
readonly "range_obstructed_title": string;
};
+ readonly "fileExplorer": {
+ readonly "search_label": string;
+ readonly "placeholder": string;
+ readonly "empty": string;
+ readonly "preview_label": string;
+ readonly "preview_loading": string;
+ readonly "preview_binary": string;
+ readonly "preview_unavailable": string;
+ readonly "preview_truncated": string;
+ };
};
export type LocaleDirection = 'ltr' | 'rtl';
@@ -176,47 +189,47 @@ export interface LocaleMetadata {
readonly direction: LocaleDirection;
}
-export const en: TranslationSchema = {"footer":{"mode":{"browse":"browse","insert":"insert","normal":"normal","preview":"preview","settings":"settings","files":"files","graft":"graft","history":"history"},"context":{"settings":"settings","graft_empty":"open a file to inspect it","history_empty":"no Echo evidence yet","history_count":"Echo evidence: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k move","enter_change":"enter change","f2_close":"f2 close","esc_close":"esc close","j_k_scroll":"j/k scroll","f3_source":"f3 source","f3_preview":"f3 preview","text_input":"text input","esc_normal":"esc normal","ctrl_s_save":"ctrl+s save","ctrl_t_theme":"ctrl+t theme","tab_focus":"tab focus","tab_indent":"tab indent","ctrl_b_files":"ctrl+b files","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h history","enter_open":"enter open","backspace_up":"backspace up","ctrl_b_close":"ctrl+b close","enter_jump":"enter jump","r_refresh":"r refresh","ctrl_g_close":"ctrl+g close","ctrl_h_close":"ctrl+h close","i_insert":"i insert","o_open_line":"o open line","cc_line":"cc line","cw_word":"cw word","ce_word_end":"ce word-end","c0_start":"c0 start","c_end":"c$ end","dd_line":"dd line","dw_word":"dw word","de_word_end":"de word-end","d0_start":"d0 start","d_end":"d$ end","yy_line":"yy line","yw_word":"yw word","ye_word_end":"ye word-end","y0_start":"y0 start","y_end":"y$ end","gg_top":"gg top","esc_cancel":"esc cancel"},"evidence":{"head":"head","tick":"tick","anchor":"anchor","coverage":"coverage","complete":"complete","partial":"partial","unavailable":"unavailable"}},"history":{"title":"Echo History","empty":"No Echo evidence yet","header":"# tick kind status evidence summary"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const en: TranslationSchema = {"footer":{"mode":{"browse":"browse","insert":"insert","normal":"normal","preview":"preview","settings":"settings","files":"files","graft":"graft","history":"history"},"context":{"settings":"settings","graft_empty":"open a file to inspect it","history_empty":"no Echo evidence yet","history_count":"Echo evidence: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k move","enter_change":"enter change","f2_close":"f2 close","esc_close":"esc close","j_k_scroll":"j/k scroll","f3_source":"f3 source","f3_preview":"f3 preview","text_input":"text input","esc_normal":"esc normal","ctrl_s_save":"ctrl+s save","ctrl_t_theme":"ctrl+t theme","tab_focus":"tab focus","tab_indent":"tab indent","ctrl_b_files":"ctrl+b files","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h history","enter_open":"enter open","backspace_up":"backspace up","ctrl_b_close":"ctrl+b close","enter_jump":"enter jump","r_refresh":"r refresh","ctrl_g_close":"ctrl+g close","ctrl_h_close":"ctrl+h close","i_insert":"i insert","o_open_line":"o open line","cc_line":"cc line","cw_word":"cw word","ce_word_end":"ce word-end","c0_start":"c0 start","c_end":"c$ end","dd_line":"dd line","dw_word":"dw word","de_word_end":"de word-end","d0_start":"d0 start","d_end":"d$ end","yy_line":"yy line","yw_word":"yw word","ye_word_end":"ye word-end","y0_start":"y0 start","y_end":"y$ end","gg_top":"gg top","esc_cancel":"esc cancel","type_filter":"type filter","arrows_select":"↑/↓ select","slash_filter":"/ filter"},"evidence":{"head":"head","tick":"tick","anchor":"anchor","coverage":"coverage","complete":"complete","partial":"partial","unavailable":"unavailable"}},"history":{"title":"Echo History","empty":"No Echo evidence yet","header":"# tick kind status evidence summary"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Search","placeholder":"Filter files…","empty":"No matches","preview_label":"Preview","preview_loading":"Loading preview…","preview_binary":"Binary file — preview unavailable","preview_unavailable":"Preview unavailable","preview_truncated":"Preview truncated"}};
-export const fr: TranslationSchema = {"footer":{"mode":{"browse":"parcourir","insert":"insertion","normal":"normal","preview":"aperçu","settings":"paramètres","files":"fichiers","graft":"graft","history":"historique"},"context":{"settings":"paramètres","graft_empty":"ouvrez un fichier pour l'inspecter","history_empty":"pas encore de preuve Echo","history_count":"Preuves Echo : {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k déplacer","enter_change":"enter changer","f2_close":"f2 fermer","esc_close":"esc fermer","j_k_scroll":"j/k défiler","f3_source":"f3 source","f3_preview":"f3 aperçu","text_input":"saisie texte","esc_normal":"esc normal","ctrl_s_save":"ctrl+s enregistrer","ctrl_t_theme":"ctrl+t thème","tab_focus":"tab focus","tab_indent":"tab retrait","ctrl_b_files":"ctrl+b fichiers","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historique","enter_open":"enter ouvrir","backspace_up":"backspace haut","ctrl_b_close":"ctrl+b fermer","enter_jump":"enter aller","r_refresh":"r actualiser","ctrl_g_close":"ctrl+g fermer","ctrl_h_close":"ctrl+h fermer","i_insert":"i insérer","o_open_line":"o ouvrir ligne","cc_line":"cc ligne","cw_word":"cw mot","ce_word_end":"ce fin mot","c0_start":"c0 début","c_end":"c$ fin","dd_line":"dd ligne","dw_word":"dw mot","de_word_end":"de fin mot","d0_start":"d0 début","d_end":"d$ fin","yy_line":"yy ligne","yw_word":"yw mot","ye_word_end":"ye fin mot","y0_start":"y0 début","y_end":"y$ fin","gg_top":"gg haut","esc_cancel":"esc annuler"},"evidence":{"head":"tête","tick":"tick","anchor":"ancre","coverage":"couverture","complete":"complète","partial":"partielle","unavailable":"indisponible"}},"history":{"title":"Historique Echo","empty":"Pas encore de preuve Echo","header":"# tick type statut preuve résumé"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const fr: TranslationSchema = {"footer":{"mode":{"browse":"parcourir","insert":"insertion","normal":"normal","preview":"aperçu","settings":"paramètres","files":"fichiers","graft":"graft","history":"historique"},"context":{"settings":"paramètres","graft_empty":"ouvrez un fichier pour l'inspecter","history_empty":"pas encore de preuve Echo","history_count":"Preuves Echo : {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k déplacer","enter_change":"enter changer","f2_close":"f2 fermer","esc_close":"esc fermer","j_k_scroll":"j/k défiler","f3_source":"f3 source","f3_preview":"f3 aperçu","text_input":"saisie texte","esc_normal":"esc normal","ctrl_s_save":"ctrl+s enregistrer","ctrl_t_theme":"ctrl+t thème","tab_focus":"tab focus","tab_indent":"tab retrait","ctrl_b_files":"ctrl+b fichiers","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historique","enter_open":"enter ouvrir","backspace_up":"backspace haut","ctrl_b_close":"ctrl+b fermer","enter_jump":"enter aller","r_refresh":"r actualiser","ctrl_g_close":"ctrl+g fermer","ctrl_h_close":"ctrl+h fermer","i_insert":"i insérer","o_open_line":"o ouvrir ligne","cc_line":"cc ligne","cw_word":"cw mot","ce_word_end":"ce fin mot","c0_start":"c0 début","c_end":"c$ fin","dd_line":"dd ligne","dw_word":"dw mot","de_word_end":"de fin mot","d0_start":"d0 début","d_end":"d$ fin","yy_line":"yy ligne","yw_word":"yw mot","ye_word_end":"ye fin mot","y0_start":"y0 début","y_end":"y$ fin","gg_top":"gg haut","esc_cancel":"esc annuler","type_filter":"saisir le filtre","arrows_select":"↑/↓ sélectionner","slash_filter":"/ filtrer"},"evidence":{"head":"tête","tick":"tick","anchor":"ancre","coverage":"couverture","complete":"complète","partial":"partielle","unavailable":"indisponible"}},"history":{"title":"Historique Echo","empty":"Pas encore de preuve Echo","header":"# tick type statut preuve résumé"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Recherche","placeholder":"Filtrer les fichiers…","empty":"Aucun résultat","preview_label":"Aperçu","preview_loading":"Chargement de l’aperçu…","preview_binary":"Fichier binaire — aperçu indisponible","preview_unavailable":"Aperçu indisponible","preview_truncated":"Aperçu tronqué"}};
-export const it: TranslationSchema = {"footer":{"mode":{"browse":"sfoglia","insert":"inserisci","normal":"normale","preview":"anteprima","settings":"impostazioni","files":"file","graft":"graft","history":"cronologia"},"context":{"settings":"impostazioni","graft_empty":"apri un file per ispezionarlo","history_empty":"nessuna prova Echo ancora","history_count":"Prove Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k muovi","enter_change":"enter cambia","f2_close":"f2 chiudi","esc_close":"esc chiudi","j_k_scroll":"j/k scorri","f3_source":"f3 sorgente","f3_preview":"f3 anteprima","text_input":"testo","esc_normal":"esc normale","ctrl_s_save":"ctrl+s salva","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab focus","tab_indent":"tab rientra","ctrl_b_files":"ctrl+b file","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h cronologia","enter_open":"enter apri","backspace_up":"backspace su","ctrl_b_close":"ctrl+b chiudi","enter_jump":"enter vai","r_refresh":"r aggiorna","ctrl_g_close":"ctrl+g chiudi","ctrl_h_close":"ctrl+h chiudi","i_insert":"i inserisci","o_open_line":"o apri riga","cc_line":"cc riga","cw_word":"cw parola","ce_word_end":"ce fine parola","c0_start":"c0 inizio","c_end":"c$ fine","dd_line":"dd riga","dw_word":"dw parola","de_word_end":"de fine parola","d0_start":"d0 inizio","d_end":"d$ fine","yy_line":"yy riga","yw_word":"yw parola","ye_word_end":"ye fine parola","y0_start":"y0 inizio","y_end":"y$ fine","gg_top":"gg cima","esc_cancel":"esc annulla"},"evidence":{"head":"testa","tick":"tick","anchor":"ancora","coverage":"copertura","complete":"completa","partial":"parziale","unavailable":"non disponibile"}},"history":{"title":"Cronologia Echo","empty":"Nessuna prova Echo ancora","header":"# tick tipo stato prova riepilogo"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const it: TranslationSchema = {"footer":{"mode":{"browse":"sfoglia","insert":"inserisci","normal":"normale","preview":"anteprima","settings":"impostazioni","files":"file","graft":"graft","history":"cronologia"},"context":{"settings":"impostazioni","graft_empty":"apri un file per ispezionarlo","history_empty":"nessuna prova Echo ancora","history_count":"Prove Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k muovi","enter_change":"enter cambia","f2_close":"f2 chiudi","esc_close":"esc chiudi","j_k_scroll":"j/k scorri","f3_source":"f3 sorgente","f3_preview":"f3 anteprima","text_input":"testo","esc_normal":"esc normale","ctrl_s_save":"ctrl+s salva","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab focus","tab_indent":"tab rientra","ctrl_b_files":"ctrl+b file","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h cronologia","enter_open":"enter apri","backspace_up":"backspace su","ctrl_b_close":"ctrl+b chiudi","enter_jump":"enter vai","r_refresh":"r aggiorna","ctrl_g_close":"ctrl+g chiudi","ctrl_h_close":"ctrl+h chiudi","i_insert":"i inserisci","o_open_line":"o apri riga","cc_line":"cc riga","cw_word":"cw parola","ce_word_end":"ce fine parola","c0_start":"c0 inizio","c_end":"c$ fine","dd_line":"dd riga","dw_word":"dw parola","de_word_end":"de fine parola","d0_start":"d0 inizio","d_end":"d$ fine","yy_line":"yy riga","yw_word":"yw parola","ye_word_end":"ye fine parola","y0_start":"y0 inizio","y_end":"y$ fine","gg_top":"gg cima","esc_cancel":"esc annulla","type_filter":"digita filtro","arrows_select":"↑/↓ seleziona","slash_filter":"/ filtra"},"evidence":{"head":"testa","tick":"tick","anchor":"ancora","coverage":"copertura","complete":"completa","partial":"parziale","unavailable":"non disponibile"}},"history":{"title":"Cronologia Echo","empty":"Nessuna prova Echo ancora","header":"# tick tipo stato prova riepilogo"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Cerca","placeholder":"Filtra file…","empty":"Nessun risultato","preview_label":"Anteprima","preview_loading":"Caricamento anteprima…","preview_binary":"File binario — anteprima non disponibile","preview_unavailable":"Anteprima non disponibile","preview_truncated":"Anteprima troncata"}};
-export const de: TranslationSchema = {"footer":{"mode":{"browse":"durchsuchen","insert":"einfügen","normal":"normal","preview":"vorschau","settings":"einstellungen","files":"dateien","graft":"graft","history":"verlauf"},"context":{"settings":"einstellungen","graft_empty":"datei öffnen zum prüfen","history_empty":"noch keine Echo-Nachweise","history_count":"Echo-Nachweise: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k bewegen","enter_change":"enter ändern","f2_close":"f2 schließen","esc_close":"esc schließen","j_k_scroll":"j/k scrollen","f3_source":"f3 quelltext","f3_preview":"f3 vorschau","text_input":"texteingabe","esc_normal":"esc normal","ctrl_s_save":"ctrl+s speichern","ctrl_t_theme":"ctrl+t theme","tab_focus":"tab fokus","tab_indent":"tab einrücken","ctrl_b_files":"ctrl+b dateien","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h verlauf","enter_open":"enter öffnen","backspace_up":"backspace hoch","ctrl_b_close":"ctrl+b schließen","enter_jump":"enter springen","r_refresh":"r aktualisieren","ctrl_g_close":"ctrl+g schließen","ctrl_h_close":"ctrl+h schließen","i_insert":"i einfügen","o_open_line":"o zeile öffnen","cc_line":"cc zeile","cw_word":"cw wort","ce_word_end":"ce wortende","c0_start":"c0 anfang","c_end":"c$ ende","dd_line":"dd zeile","dw_word":"dw wort","de_word_end":"de wortende","d0_start":"d0 anfang","d_end":"d$ ende","yy_line":"yy zeile","yw_word":"yw wort","ye_word_end":"ye wortende","y0_start":"y0 anfang","y_end":"y$ ende","gg_top":"gg oben","esc_cancel":"esc abbrechen"},"evidence":{"head":"Kopf","tick":"Tick","anchor":"Anker","coverage":"Abdeckung","complete":"vollständig","partial":"teilweise","unavailable":"nicht verfügbar"}},"history":{"title":"Echo-Verlauf","empty":"Noch keine Echo-Nachweise","header":"# tick art status nachweis zusammenfassung"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const de: TranslationSchema = {"footer":{"mode":{"browse":"durchsuchen","insert":"einfügen","normal":"normal","preview":"vorschau","settings":"einstellungen","files":"dateien","graft":"graft","history":"verlauf"},"context":{"settings":"einstellungen","graft_empty":"datei öffnen zum prüfen","history_empty":"noch keine Echo-Nachweise","history_count":"Echo-Nachweise: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k bewegen","enter_change":"enter ändern","f2_close":"f2 schließen","esc_close":"esc schließen","j_k_scroll":"j/k scrollen","f3_source":"f3 quelltext","f3_preview":"f3 vorschau","text_input":"texteingabe","esc_normal":"esc normal","ctrl_s_save":"ctrl+s speichern","ctrl_t_theme":"ctrl+t theme","tab_focus":"tab fokus","tab_indent":"tab einrücken","ctrl_b_files":"ctrl+b dateien","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h verlauf","enter_open":"enter öffnen","backspace_up":"backspace hoch","ctrl_b_close":"ctrl+b schließen","enter_jump":"enter springen","r_refresh":"r aktualisieren","ctrl_g_close":"ctrl+g schließen","ctrl_h_close":"ctrl+h schließen","i_insert":"i einfügen","o_open_line":"o zeile öffnen","cc_line":"cc zeile","cw_word":"cw wort","ce_word_end":"ce wortende","c0_start":"c0 anfang","c_end":"c$ ende","dd_line":"dd zeile","dw_word":"dw wort","de_word_end":"de wortende","d0_start":"d0 anfang","d_end":"d$ ende","yy_line":"yy zeile","yw_word":"yw wort","ye_word_end":"ye wortende","y0_start":"y0 anfang","y_end":"y$ ende","gg_top":"gg oben","esc_cancel":"esc abbrechen","type_filter":"Filter eingeben","arrows_select":"↑/↓ auswählen","slash_filter":"/ filtern"},"evidence":{"head":"Kopf","tick":"Tick","anchor":"Anker","coverage":"Abdeckung","complete":"vollständig","partial":"teilweise","unavailable":"nicht verfügbar"}},"history":{"title":"Echo-Verlauf","empty":"Noch keine Echo-Nachweise","header":"# tick art status nachweis zusammenfassung"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Suche","placeholder":"Dateien filtern…","empty":"Keine Treffer","preview_label":"Vorschau","preview_loading":"Vorschau wird geladen…","preview_binary":"Binärdatei — keine Vorschau","preview_unavailable":"Vorschau nicht verfügbar","preview_truncated":"Vorschau gekürzt"}};
-export const es: TranslationSchema = {"footer":{"mode":{"browse":"explorar","insert":"insertar","normal":"normal","preview":"vista previa","settings":"ajustes","files":"archivos","graft":"graft","history":"historial"},"context":{"settings":"ajustes","graft_empty":"abre un archivo para inspeccionarlo","history_empty":"sin evidencia Echo todavía","history_count":"Evidencia Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k mover","enter_change":"enter cambiar","f2_close":"f2 cerrar","esc_close":"esc cerrar","j_k_scroll":"j/k desplazar","f3_source":"f3 fuente","f3_preview":"f3 vista previa","text_input":"texto","esc_normal":"esc normal","ctrl_s_save":"ctrl+s guardar","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab foco","tab_indent":"tab sangría","ctrl_b_files":"ctrl+b archivos","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historial","enter_open":"enter abrir","backspace_up":"backspace arriba","ctrl_b_close":"ctrl+b cerrar","enter_jump":"enter saltar","r_refresh":"r actualizar","ctrl_g_close":"ctrl+g cerrar","ctrl_h_close":"ctrl+h cerrar","i_insert":"i insertar","o_open_line":"o abrir línea","cc_line":"cc línea","cw_word":"cw palabra","ce_word_end":"ce fin palabra","c0_start":"c0 inicio","c_end":"c$ fin","dd_line":"dd línea","dw_word":"dw palabra","de_word_end":"de fin palabra","d0_start":"d0 inicio","d_end":"d$ fin","yy_line":"yy línea","yw_word":"yw palabra","ye_word_end":"ye fin palabra","y0_start":"y0 inicio","y_end":"y$ fin","gg_top":"gg arriba","esc_cancel":"esc cancelar"},"evidence":{"head":"cabeza","tick":"tick","anchor":"ancla","coverage":"cobertura","complete":"completa","partial":"parcial","unavailable":"no disponible"}},"history":{"title":"Historial Echo","empty":"Sin evidencia Echo todavía","header":"# tick tipo estado evidencia resumen"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const es: TranslationSchema = {"footer":{"mode":{"browse":"explorar","insert":"insertar","normal":"normal","preview":"vista previa","settings":"ajustes","files":"archivos","graft":"graft","history":"historial"},"context":{"settings":"ajustes","graft_empty":"abre un archivo para inspeccionarlo","history_empty":"sin evidencia Echo todavía","history_count":"Evidencia Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k mover","enter_change":"enter cambiar","f2_close":"f2 cerrar","esc_close":"esc cerrar","j_k_scroll":"j/k desplazar","f3_source":"f3 fuente","f3_preview":"f3 vista previa","text_input":"texto","esc_normal":"esc normal","ctrl_s_save":"ctrl+s guardar","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab foco","tab_indent":"tab sangría","ctrl_b_files":"ctrl+b archivos","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historial","enter_open":"enter abrir","backspace_up":"backspace arriba","ctrl_b_close":"ctrl+b cerrar","enter_jump":"enter saltar","r_refresh":"r actualizar","ctrl_g_close":"ctrl+g cerrar","ctrl_h_close":"ctrl+h cerrar","i_insert":"i insertar","o_open_line":"o abrir línea","cc_line":"cc línea","cw_word":"cw palabra","ce_word_end":"ce fin palabra","c0_start":"c0 inicio","c_end":"c$ fin","dd_line":"dd línea","dw_word":"dw palabra","de_word_end":"de fin palabra","d0_start":"d0 inicio","d_end":"d$ fin","yy_line":"yy línea","yw_word":"yw palabra","ye_word_end":"ye fin palabra","y0_start":"y0 inicio","y_end":"y$ fin","gg_top":"gg arriba","esc_cancel":"esc cancelar","type_filter":"escribir filtro","arrows_select":"↑/↓ seleccionar","slash_filter":"/ filtrar"},"evidence":{"head":"cabeza","tick":"tick","anchor":"ancla","coverage":"cobertura","complete":"completa","partial":"parcial","unavailable":"no disponible"}},"history":{"title":"Historial Echo","empty":"Sin evidencia Echo todavía","header":"# tick tipo estado evidencia resumen"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Buscar","placeholder":"Filtrar archivos…","empty":"Sin resultados","preview_label":"Vista previa","preview_loading":"Cargando vista previa…","preview_binary":"Archivo binario — sin vista previa","preview_unavailable":"Vista previa no disponible","preview_truncated":"Vista previa truncada"}};
-export const ko: TranslationSchema = {"footer":{"mode":{"browse":"탐색","insert":"삽입","normal":"일반","preview":"미리보기","settings":"설정","files":"파일","graft":"Graft","history":"기록"},"context":{"settings":"설정","graft_empty":"검사할 파일을 여세요","history_empty":"아직 Echo 증거 없음","history_count":"Echo 증거: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 이동","enter_change":"enter 변경","f2_close":"f2 닫기","esc_close":"esc 닫기","j_k_scroll":"j/k 스크롤","f3_source":"f3 소스","f3_preview":"f3 미리보기","text_input":"텍스트 입력","esc_normal":"esc 일반","ctrl_s_save":"ctrl+s 저장","ctrl_t_theme":"ctrl+t 테마","tab_focus":"tab 포커스","tab_indent":"tab 들여쓰기","ctrl_b_files":"ctrl+b 파일","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 기록","enter_open":"enter 열기","backspace_up":"backspace 위로","ctrl_b_close":"ctrl+b 닫기","enter_jump":"enter 이동","r_refresh":"r 새로고침","ctrl_g_close":"ctrl+g 닫기","ctrl_h_close":"ctrl+h 닫기","i_insert":"i 삽입","o_open_line":"o 줄 열기","cc_line":"cc 줄","cw_word":"cw 단어","ce_word_end":"ce 단어 끝","c0_start":"c0 시작","c_end":"c$ 끝","dd_line":"dd 줄","dw_word":"dw 단어","de_word_end":"de 단어 끝","d0_start":"d0 시작","d_end":"d$ 끝","yy_line":"yy 줄","yw_word":"yw 단어","ye_word_end":"ye 단어 끝","y0_start":"y0 시작","y_end":"y$ 끝","gg_top":"gg 맨 위","esc_cancel":"esc 취소"},"evidence":{"head":"헤드","tick":"틱","anchor":"앵커","coverage":"범위","complete":"완전","partial":"부분","unavailable":"사용 불가"}},"history":{"title":"Echo 기록","empty":"아직 Echo 증거 없음","header":"# tick 종류 상태 증거 요약"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const ko: TranslationSchema = {"footer":{"mode":{"browse":"탐색","insert":"삽입","normal":"일반","preview":"미리보기","settings":"설정","files":"파일","graft":"Graft","history":"기록"},"context":{"settings":"설정","graft_empty":"검사할 파일을 여세요","history_empty":"아직 Echo 증거 없음","history_count":"Echo 증거: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 이동","enter_change":"enter 변경","f2_close":"f2 닫기","esc_close":"esc 닫기","j_k_scroll":"j/k 스크롤","f3_source":"f3 소스","f3_preview":"f3 미리보기","text_input":"텍스트 입력","esc_normal":"esc 일반","ctrl_s_save":"ctrl+s 저장","ctrl_t_theme":"ctrl+t 테마","tab_focus":"tab 포커스","tab_indent":"tab 들여쓰기","ctrl_b_files":"ctrl+b 파일","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 기록","enter_open":"enter 열기","backspace_up":"backspace 위로","ctrl_b_close":"ctrl+b 닫기","enter_jump":"enter 이동","r_refresh":"r 새로고침","ctrl_g_close":"ctrl+g 닫기","ctrl_h_close":"ctrl+h 닫기","i_insert":"i 삽입","o_open_line":"o 줄 열기","cc_line":"cc 줄","cw_word":"cw 단어","ce_word_end":"ce 단어 끝","c0_start":"c0 시작","c_end":"c$ 끝","dd_line":"dd 줄","dw_word":"dw 단어","de_word_end":"de 단어 끝","d0_start":"d0 시작","d_end":"d$ 끝","yy_line":"yy 줄","yw_word":"yw 단어","ye_word_end":"ye 단어 끝","y0_start":"y0 시작","y_end":"y$ 끝","gg_top":"gg 맨 위","esc_cancel":"esc 취소","type_filter":"필터 입력","arrows_select":"↑/↓ 선택","slash_filter":"/ 필터"},"evidence":{"head":"헤드","tick":"틱","anchor":"앵커","coverage":"범위","complete":"완전","partial":"부분","unavailable":"사용 불가"}},"history":{"title":"Echo 기록","empty":"아직 Echo 증거 없음","header":"# tick 종류 상태 증거 요약"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"검색","placeholder":"파일 필터…","empty":"일치 항목 없음","preview_label":"미리 보기","preview_loading":"미리 보기 불러오는 중…","preview_binary":"바이너리 파일 — 미리 보기 불가","preview_unavailable":"미리 보기 불가","preview_truncated":"미리 보기 생략됨"}};
-export const ja: TranslationSchema = {"footer":{"mode":{"browse":"参照","insert":"挿入","normal":"通常","preview":"プレビュー","settings":"設定","files":"ファイル","graft":"Graft","history":"履歴"},"context":{"settings":"設定","graft_empty":"検査するファイルを開く","history_empty":"Echo 証拠はまだありません","history_count":"Echo 証拠: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 移動","enter_change":"enter 変更","f2_close":"f2 閉じる","esc_close":"esc 閉じる","j_k_scroll":"j/k スクロール","f3_source":"f3 ソース","f3_preview":"f3 プレビュー","text_input":"テキスト入力","esc_normal":"esc 通常","ctrl_s_save":"ctrl+s 保存","ctrl_t_theme":"ctrl+t テーマ","tab_focus":"tab フォーカス","tab_indent":"tab インデント","ctrl_b_files":"ctrl+b ファイル","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 履歴","enter_open":"enter 開く","backspace_up":"backspace 上へ","ctrl_b_close":"ctrl+b 閉じる","enter_jump":"enter 移動","r_refresh":"r 更新","ctrl_g_close":"ctrl+g 閉じる","ctrl_h_close":"ctrl+h 閉じる","i_insert":"i 挿入","o_open_line":"o 行を開く","cc_line":"cc 行","cw_word":"cw 単語","ce_word_end":"ce 単語末","c0_start":"c0 先頭","c_end":"c$ 末尾","dd_line":"dd 行","dw_word":"dw 単語","de_word_end":"de 単語末","d0_start":"d0 先頭","d_end":"d$ 末尾","yy_line":"yy 行","yw_word":"yw 単語","ye_word_end":"ye 単語末","y0_start":"y0 先頭","y_end":"y$ 末尾","gg_top":"gg 先頭","esc_cancel":"esc 取消"},"evidence":{"head":"ヘッド","tick":"ティック","anchor":"アンカー","coverage":"範囲","complete":"完全","partial":"部分","unavailable":"利用不可"}},"history":{"title":"Echo 履歴","empty":"Echo 証拠はまだありません","header":"# tick 種類 状態 証拠 概要"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const ja: TranslationSchema = {"footer":{"mode":{"browse":"参照","insert":"挿入","normal":"通常","preview":"プレビュー","settings":"設定","files":"ファイル","graft":"Graft","history":"履歴"},"context":{"settings":"設定","graft_empty":"検査するファイルを開く","history_empty":"Echo 証拠はまだありません","history_count":"Echo 証拠: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 移動","enter_change":"enter 変更","f2_close":"f2 閉じる","esc_close":"esc 閉じる","j_k_scroll":"j/k スクロール","f3_source":"f3 ソース","f3_preview":"f3 プレビュー","text_input":"テキスト入力","esc_normal":"esc 通常","ctrl_s_save":"ctrl+s 保存","ctrl_t_theme":"ctrl+t テーマ","tab_focus":"tab フォーカス","tab_indent":"tab インデント","ctrl_b_files":"ctrl+b ファイル","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 履歴","enter_open":"enter 開く","backspace_up":"backspace 上へ","ctrl_b_close":"ctrl+b 閉じる","enter_jump":"enter 移動","r_refresh":"r 更新","ctrl_g_close":"ctrl+g 閉じる","ctrl_h_close":"ctrl+h 閉じる","i_insert":"i 挿入","o_open_line":"o 行を開く","cc_line":"cc 行","cw_word":"cw 単語","ce_word_end":"ce 単語末","c0_start":"c0 先頭","c_end":"c$ 末尾","dd_line":"dd 行","dw_word":"dw 単語","de_word_end":"de 単語末","d0_start":"d0 先頭","d_end":"d$ 末尾","yy_line":"yy 行","yw_word":"yw 単語","ye_word_end":"ye 単語末","y0_start":"y0 先頭","y_end":"y$ 末尾","gg_top":"gg 先頭","esc_cancel":"esc 取消","type_filter":"絞り込み入力","arrows_select":"↑/↓ 選択","slash_filter":"/ 絞り込み"},"evidence":{"head":"ヘッド","tick":"ティック","anchor":"アンカー","coverage":"範囲","complete":"完全","partial":"部分","unavailable":"利用不可"}},"history":{"title":"Echo 履歴","empty":"Echo 証拠はまだありません","header":"# tick 種類 状態 証拠 概要"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"検索","placeholder":"ファイルを絞り込む…","empty":"一致なし","preview_label":"プレビュー","preview_loading":"プレビューを読み込み中…","preview_binary":"バイナリファイル — プレビュー不可","preview_unavailable":"プレビュー不可","preview_truncated":"プレビューを省略"}};
-export const zhHans: TranslationSchema = {"footer":{"mode":{"browse":"浏览","insert":"插入","normal":"普通","preview":"预览","settings":"设置","files":"文件","graft":"Graft","history":"历史"},"context":{"settings":"设置","graft_empty":"打开文件以检查","history_empty":"还没有 Echo 证据","history_count":"Echo 证据: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 移动","enter_change":"enter 更改","f2_close":"f2 关闭","esc_close":"esc 关闭","j_k_scroll":"j/k 滚动","f3_source":"f3 源码","f3_preview":"f3 预览","text_input":"文本输入","esc_normal":"esc 普通","ctrl_s_save":"ctrl+s 保存","ctrl_t_theme":"ctrl+t 主题","tab_focus":"tab 焦点","tab_indent":"tab 缩进","ctrl_b_files":"ctrl+b 文件","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 历史","enter_open":"enter 打开","backspace_up":"backspace 上一级","ctrl_b_close":"ctrl+b 关闭","enter_jump":"enter 跳转","r_refresh":"r 刷新","ctrl_g_close":"ctrl+g 关闭","ctrl_h_close":"ctrl+h 关闭","i_insert":"i 插入","o_open_line":"o 打开行","cc_line":"cc 行","cw_word":"cw 词","ce_word_end":"ce 词尾","c0_start":"c0 开头","c_end":"c$ 结尾","dd_line":"dd 行","dw_word":"dw 词","de_word_end":"de 词尾","d0_start":"d0 开头","d_end":"d$ 结尾","yy_line":"yy 行","yw_word":"yw 词","ye_word_end":"ye 词尾","y0_start":"y0 开头","y_end":"y$ 结尾","gg_top":"gg 顶部","esc_cancel":"esc 取消"},"evidence":{"head":"头","tick":"时刻","anchor":"锚点","coverage":"覆盖","complete":"完整","partial":"部分","unavailable":"不可用"}},"history":{"title":"Echo 历史","empty":"还没有 Echo 证据","header":"# tick 类型 状态 证据 摘要"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const zhHans: TranslationSchema = {"footer":{"mode":{"browse":"浏览","insert":"插入","normal":"普通","preview":"预览","settings":"设置","files":"文件","graft":"Graft","history":"历史"},"context":{"settings":"设置","graft_empty":"打开文件以检查","history_empty":"还没有 Echo 证据","history_count":"Echo 证据: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 移动","enter_change":"enter 更改","f2_close":"f2 关闭","esc_close":"esc 关闭","j_k_scroll":"j/k 滚动","f3_source":"f3 源码","f3_preview":"f3 预览","text_input":"文本输入","esc_normal":"esc 普通","ctrl_s_save":"ctrl+s 保存","ctrl_t_theme":"ctrl+t 主题","tab_focus":"tab 焦点","tab_indent":"tab 缩进","ctrl_b_files":"ctrl+b 文件","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 历史","enter_open":"enter 打开","backspace_up":"backspace 上一级","ctrl_b_close":"ctrl+b 关闭","enter_jump":"enter 跳转","r_refresh":"r 刷新","ctrl_g_close":"ctrl+g 关闭","ctrl_h_close":"ctrl+h 关闭","i_insert":"i 插入","o_open_line":"o 打开行","cc_line":"cc 行","cw_word":"cw 词","ce_word_end":"ce 词尾","c0_start":"c0 开头","c_end":"c$ 结尾","dd_line":"dd 行","dw_word":"dw 词","de_word_end":"de 词尾","d0_start":"d0 开头","d_end":"d$ 结尾","yy_line":"yy 行","yw_word":"yw 词","ye_word_end":"ye 词尾","y0_start":"y0 开头","y_end":"y$ 结尾","gg_top":"gg 顶部","esc_cancel":"esc 取消","type_filter":"输入筛选","arrows_select":"↑/↓ 选择","slash_filter":"/ 筛选"},"evidence":{"head":"头","tick":"时刻","anchor":"锚点","coverage":"覆盖","complete":"完整","partial":"部分","unavailable":"不可用"}},"history":{"title":"Echo 历史","empty":"还没有 Echo 证据","header":"# tick 类型 状态 证据 摘要"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"搜索","placeholder":"筛选文件…","empty":"无匹配项","preview_label":"预览","preview_loading":"正在加载预览…","preview_binary":"二进制文件 — 无法预览","preview_unavailable":"无法预览","preview_truncated":"预览已截断"}};
-export const ptBR: TranslationSchema = {"footer":{"mode":{"browse":"navegar","insert":"inserir","normal":"normal","preview":"prévia","settings":"configurações","files":"arquivos","graft":"graft","history":"histórico"},"context":{"settings":"configurações","graft_empty":"abra um arquivo para inspecionar","history_empty":"nenhuma evidência Echo ainda","history_count":"Evidências Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k mover","enter_change":"enter alterar","f2_close":"f2 fechar","esc_close":"esc fechar","j_k_scroll":"j/k rolar","f3_source":"f3 código","f3_preview":"f3 prévia","text_input":"entrada de texto","esc_normal":"esc normal","ctrl_s_save":"ctrl+s salvar","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab foco","tab_indent":"tab recuo","ctrl_b_files":"ctrl+b arquivos","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h histórico","enter_open":"enter abrir","backspace_up":"backspace subir","ctrl_b_close":"ctrl+b fechar","enter_jump":"enter ir","r_refresh":"r atualizar","ctrl_g_close":"ctrl+g fechar","ctrl_h_close":"ctrl+h fechar","i_insert":"i inserir","o_open_line":"o abrir linha","cc_line":"cc linha","cw_word":"cw palavra","ce_word_end":"ce fim palavra","c0_start":"c0 início","c_end":"c$ fim","dd_line":"dd linha","dw_word":"dw palavra","de_word_end":"de fim palavra","d0_start":"d0 início","d_end":"d$ fim","yy_line":"yy linha","yw_word":"yw palavra","ye_word_end":"ye fim palavra","y0_start":"y0 início","y_end":"y$ fim","gg_top":"gg topo","esc_cancel":"esc cancelar"},"evidence":{"head":"cabeça","tick":"tick","anchor":"âncora","coverage":"cobertura","complete":"completa","partial":"parcial","unavailable":"indisponível"}},"history":{"title":"Histórico Echo","empty":"Nenhuma evidência Echo ainda","header":"# tick tipo status evidência resumo"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const ptBR: TranslationSchema = {"footer":{"mode":{"browse":"navegar","insert":"inserir","normal":"normal","preview":"prévia","settings":"configurações","files":"arquivos","graft":"graft","history":"histórico"},"context":{"settings":"configurações","graft_empty":"abra um arquivo para inspecionar","history_empty":"nenhuma evidência Echo ainda","history_count":"Evidências Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k mover","enter_change":"enter alterar","f2_close":"f2 fechar","esc_close":"esc fechar","j_k_scroll":"j/k rolar","f3_source":"f3 código","f3_preview":"f3 prévia","text_input":"entrada de texto","esc_normal":"esc normal","ctrl_s_save":"ctrl+s salvar","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab foco","tab_indent":"tab recuo","ctrl_b_files":"ctrl+b arquivos","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h histórico","enter_open":"enter abrir","backspace_up":"backspace subir","ctrl_b_close":"ctrl+b fechar","enter_jump":"enter ir","r_refresh":"r atualizar","ctrl_g_close":"ctrl+g fechar","ctrl_h_close":"ctrl+h fechar","i_insert":"i inserir","o_open_line":"o abrir linha","cc_line":"cc linha","cw_word":"cw palavra","ce_word_end":"ce fim palavra","c0_start":"c0 início","c_end":"c$ fim","dd_line":"dd linha","dw_word":"dw palavra","de_word_end":"de fim palavra","d0_start":"d0 início","d_end":"d$ fim","yy_line":"yy linha","yw_word":"yw palavra","ye_word_end":"ye fim palavra","y0_start":"y0 início","y_end":"y$ fim","gg_top":"gg topo","esc_cancel":"esc cancelar","type_filter":"digitar filtro","arrows_select":"↑/↓ selecionar","slash_filter":"/ filtrar"},"evidence":{"head":"cabeça","tick":"tick","anchor":"âncora","coverage":"cobertura","complete":"completa","partial":"parcial","unavailable":"indisponível"}},"history":{"title":"Histórico Echo","empty":"Nenhuma evidência Echo ainda","header":"# tick tipo status evidência resumo"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Buscar","placeholder":"Filtrar arquivos…","empty":"Nenhum resultado","preview_label":"Prévia","preview_loading":"Carregando prévia…","preview_binary":"Arquivo binário — sem prévia","preview_unavailable":"Prévia indisponível","preview_truncated":"Prévia truncada"}};
-export const ru: TranslationSchema = {"footer":{"mode":{"browse":"обзор","insert":"вставка","normal":"обычный","preview":"просмотр","settings":"настройки","files":"файлы","graft":"graft","history":"история"},"context":{"settings":"настройки","graft_empty":"откройте файл для проверки","history_empty":"пока нет доказательств Echo","history_count":"Доказательства Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k переместить","enter_change":"enter изменить","f2_close":"f2 закрыть","esc_close":"esc закрыть","j_k_scroll":"j/k прокрутка","f3_source":"f3 исходник","f3_preview":"f3 просмотр","text_input":"ввод текста","esc_normal":"esc обычный","ctrl_s_save":"ctrl+s сохранить","ctrl_t_theme":"ctrl+t тема","tab_focus":"tab фокус","tab_indent":"tab отступ","ctrl_b_files":"ctrl+b файлы","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h история","enter_open":"enter открыть","backspace_up":"backspace вверх","ctrl_b_close":"ctrl+b закрыть","enter_jump":"enter перейти","r_refresh":"r обновить","ctrl_g_close":"ctrl+g закрыть","ctrl_h_close":"ctrl+h закрыть","i_insert":"i вставка","o_open_line":"o открыть строку","cc_line":"cc строка","cw_word":"cw слово","ce_word_end":"ce конец слова","c0_start":"c0 начало","c_end":"c$ конец","dd_line":"dd строка","dw_word":"dw слово","de_word_end":"de конец слова","d0_start":"d0 начало","d_end":"d$ конец","yy_line":"yy строка","yw_word":"yw слово","ye_word_end":"ye конец слова","y0_start":"y0 начало","y_end":"y$ конец","gg_top":"gg верх","esc_cancel":"esc отмена"},"evidence":{"head":"вершина","tick":"тик","anchor":"якорь","coverage":"покрытие","complete":"полное","partial":"частичное","unavailable":"недоступно"}},"history":{"title":"История Echo","empty":"Пока нет доказательств Echo","header":"# tick тип статус доказательство сводка"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const ru: TranslationSchema = {"footer":{"mode":{"browse":"обзор","insert":"вставка","normal":"обычный","preview":"просмотр","settings":"настройки","files":"файлы","graft":"graft","history":"история"},"context":{"settings":"настройки","graft_empty":"откройте файл для проверки","history_empty":"пока нет доказательств Echo","history_count":"Доказательства Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k переместить","enter_change":"enter изменить","f2_close":"f2 закрыть","esc_close":"esc закрыть","j_k_scroll":"j/k прокрутка","f3_source":"f3 исходник","f3_preview":"f3 просмотр","text_input":"ввод текста","esc_normal":"esc обычный","ctrl_s_save":"ctrl+s сохранить","ctrl_t_theme":"ctrl+t тема","tab_focus":"tab фокус","tab_indent":"tab отступ","ctrl_b_files":"ctrl+b файлы","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h история","enter_open":"enter открыть","backspace_up":"backspace вверх","ctrl_b_close":"ctrl+b закрыть","enter_jump":"enter перейти","r_refresh":"r обновить","ctrl_g_close":"ctrl+g закрыть","ctrl_h_close":"ctrl+h закрыть","i_insert":"i вставка","o_open_line":"o открыть строку","cc_line":"cc строка","cw_word":"cw слово","ce_word_end":"ce конец слова","c0_start":"c0 начало","c_end":"c$ конец","dd_line":"dd строка","dw_word":"dw слово","de_word_end":"de конец слова","d0_start":"d0 начало","d_end":"d$ конец","yy_line":"yy строка","yw_word":"yw слово","ye_word_end":"ye конец слова","y0_start":"y0 начало","y_end":"y$ конец","gg_top":"gg верх","esc_cancel":"esc отмена","type_filter":"ввод фильтра","arrows_select":"↑/↓ выбор","slash_filter":"/ фильтр"},"evidence":{"head":"вершина","tick":"тик","anchor":"якорь","coverage":"покрытие","complete":"полное","partial":"частичное","unavailable":"недоступно"}},"history":{"title":"История Echo","empty":"Пока нет доказательств Echo","header":"# tick тип статус доказательство сводка"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Поиск","placeholder":"Фильтр файлов…","empty":"Нет совпадений","preview_label":"Просмотр","preview_loading":"Загрузка просмотра…","preview_binary":"Двоичный файл — просмотр недоступен","preview_unavailable":"Просмотр недоступен","preview_truncated":"Просмотр сокращён"}};
-export const ar: TranslationSchema = {"footer":{"mode":{"browse":"تصفح","insert":"إدراج","normal":"عادي","preview":"معاينة","settings":"إعدادات","files":"ملفات","graft":"Graft","history":"السجل"},"context":{"settings":"إعدادات","graft_empty":"افتح ملفا لفحصه","history_empty":"لا توجد أدلة Echo بعد","history_count":"أدلة Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k تحريك","enter_change":"enter تغيير","f2_close":"f2 إغلاق","esc_close":"esc إغلاق","j_k_scroll":"j/k تمرير","f3_source":"f3 المصدر","f3_preview":"f3 معاينة","text_input":"إدخال نص","esc_normal":"esc عادي","ctrl_s_save":"ctrl+s حفظ","ctrl_t_theme":"ctrl+t سمة","tab_focus":"tab تركيز","tab_indent":"tab إزاحة","ctrl_b_files":"ctrl+b ملفات","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h سجل","enter_open":"enter فتح","backspace_up":"backspace أعلى","ctrl_b_close":"ctrl+b إغلاق","enter_jump":"enter انتقال","r_refresh":"r تحديث","ctrl_g_close":"ctrl+g إغلاق","ctrl_h_close":"ctrl+h إغلاق","i_insert":"i إدراج","o_open_line":"o فتح سطر","cc_line":"cc سطر","cw_word":"cw كلمة","ce_word_end":"ce نهاية كلمة","c0_start":"c0 بداية","c_end":"c$ نهاية","dd_line":"dd سطر","dw_word":"dw كلمة","de_word_end":"de نهاية كلمة","d0_start":"d0 بداية","d_end":"d$ نهاية","yy_line":"yy سطر","yw_word":"yw كلمة","ye_word_end":"ye نهاية كلمة","y0_start":"y0 بداية","y_end":"y$ نهاية","gg_top":"gg أعلى","esc_cancel":"esc إلغاء"},"evidence":{"head":"الرأس","tick":"النبضة","anchor":"المرساة","coverage":"التغطية","complete":"كاملة","partial":"جزئية","unavailable":"غير متاحة"}},"history":{"title":"سجل Echo","empty":"لا توجد أدلة Echo بعد","header":"# tick النوع الحالة الدليل الملخص"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const ar: TranslationSchema = {"footer":{"mode":{"browse":"تصفح","insert":"إدراج","normal":"عادي","preview":"معاينة","settings":"إعدادات","files":"ملفات","graft":"Graft","history":"السجل"},"context":{"settings":"إعدادات","graft_empty":"افتح ملفا لفحصه","history_empty":"لا توجد أدلة Echo بعد","history_count":"أدلة Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k تحريك","enter_change":"enter تغيير","f2_close":"f2 إغلاق","esc_close":"esc إغلاق","j_k_scroll":"j/k تمرير","f3_source":"f3 المصدر","f3_preview":"f3 معاينة","text_input":"إدخال نص","esc_normal":"esc عادي","ctrl_s_save":"ctrl+s حفظ","ctrl_t_theme":"ctrl+t سمة","tab_focus":"tab تركيز","tab_indent":"tab إزاحة","ctrl_b_files":"ctrl+b ملفات","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h سجل","enter_open":"enter فتح","backspace_up":"backspace أعلى","ctrl_b_close":"ctrl+b إغلاق","enter_jump":"enter انتقال","r_refresh":"r تحديث","ctrl_g_close":"ctrl+g إغلاق","ctrl_h_close":"ctrl+h إغلاق","i_insert":"i إدراج","o_open_line":"o فتح سطر","cc_line":"cc سطر","cw_word":"cw كلمة","ce_word_end":"ce نهاية كلمة","c0_start":"c0 بداية","c_end":"c$ نهاية","dd_line":"dd سطر","dw_word":"dw كلمة","de_word_end":"de نهاية كلمة","d0_start":"d0 بداية","d_end":"d$ نهاية","yy_line":"yy سطر","yw_word":"yw كلمة","ye_word_end":"ye نهاية كلمة","y0_start":"y0 بداية","y_end":"y$ نهاية","gg_top":"gg أعلى","esc_cancel":"esc إلغاء","type_filter":"اكتب للتصفية","arrows_select":"↑/↓ تحديد","slash_filter":"/ تصفية"},"evidence":{"head":"الرأس","tick":"النبضة","anchor":"المرساة","coverage":"التغطية","complete":"كاملة","partial":"جزئية","unavailable":"غير متاحة"}},"history":{"title":"سجل Echo","empty":"لا توجد أدلة Echo بعد","header":"# tick النوع الحالة الدليل الملخص"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"بحث","placeholder":"تصفية الملفات…","empty":"لا نتائج","preview_label":"معاينة","preview_loading":"جارٍ تحميل المعاينة…","preview_binary":"ملف ثنائي — المعاينة غير متاحة","preview_unavailable":"المعاينة غير متاحة","preview_truncated":"المعاينة مختصرة"}};
-export const id: TranslationSchema = {"footer":{"mode":{"browse":"jelajah","insert":"sisip","normal":"normal","preview":"pratinjau","settings":"pengaturan","files":"berkas","graft":"graft","history":"riwayat"},"context":{"settings":"pengaturan","graft_empty":"buka berkas untuk diperiksa","history_empty":"belum ada bukti Echo","history_count":"Bukti Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k pindah","enter_change":"enter ubah","f2_close":"f2 tutup","esc_close":"esc tutup","j_k_scroll":"j/k gulir","f3_source":"f3 sumber","f3_preview":"f3 pratinjau","text_input":"input teks","esc_normal":"esc normal","ctrl_s_save":"ctrl+s simpan","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab fokus","tab_indent":"tab inden","ctrl_b_files":"ctrl+b berkas","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h riwayat","enter_open":"enter buka","backspace_up":"backspace naik","ctrl_b_close":"ctrl+b tutup","enter_jump":"enter lompat","r_refresh":"r segarkan","ctrl_g_close":"ctrl+g tutup","ctrl_h_close":"ctrl+h tutup","i_insert":"i sisip","o_open_line":"o buka baris","cc_line":"cc baris","cw_word":"cw kata","ce_word_end":"ce akhir kata","c0_start":"c0 awal","c_end":"c$ akhir","dd_line":"dd baris","dw_word":"dw kata","de_word_end":"de akhir kata","d0_start":"d0 awal","d_end":"d$ akhir","yy_line":"yy baris","yw_word":"yw kata","ye_word_end":"ye akhir kata","y0_start":"y0 awal","y_end":"y$ akhir","gg_top":"gg atas","esc_cancel":"esc batal"},"evidence":{"head":"kepala","tick":"tick","anchor":"jangkar","coverage":"cakupan","complete":"lengkap","partial":"sebagian","unavailable":"tidak tersedia"}},"history":{"title":"Riwayat Echo","empty":"Belum ada bukti Echo","header":"# tick jenis status bukti ringkasan"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const id: TranslationSchema = {"footer":{"mode":{"browse":"jelajah","insert":"sisip","normal":"normal","preview":"pratinjau","settings":"pengaturan","files":"berkas","graft":"graft","history":"riwayat"},"context":{"settings":"pengaturan","graft_empty":"buka berkas untuk diperiksa","history_empty":"belum ada bukti Echo","history_count":"Bukti Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k pindah","enter_change":"enter ubah","f2_close":"f2 tutup","esc_close":"esc tutup","j_k_scroll":"j/k gulir","f3_source":"f3 sumber","f3_preview":"f3 pratinjau","text_input":"input teks","esc_normal":"esc normal","ctrl_s_save":"ctrl+s simpan","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab fokus","tab_indent":"tab inden","ctrl_b_files":"ctrl+b berkas","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h riwayat","enter_open":"enter buka","backspace_up":"backspace naik","ctrl_b_close":"ctrl+b tutup","enter_jump":"enter lompat","r_refresh":"r segarkan","ctrl_g_close":"ctrl+g tutup","ctrl_h_close":"ctrl+h tutup","i_insert":"i sisip","o_open_line":"o buka baris","cc_line":"cc baris","cw_word":"cw kata","ce_word_end":"ce akhir kata","c0_start":"c0 awal","c_end":"c$ akhir","dd_line":"dd baris","dw_word":"dw kata","de_word_end":"de akhir kata","d0_start":"d0 awal","d_end":"d$ akhir","yy_line":"yy baris","yw_word":"yw kata","ye_word_end":"ye akhir kata","y0_start":"y0 awal","y_end":"y$ akhir","gg_top":"gg atas","esc_cancel":"esc batal","type_filter":"ketik filter","arrows_select":"↑/↓ pilih","slash_filter":"/ filter"},"evidence":{"head":"kepala","tick":"tick","anchor":"jangkar","coverage":"cakupan","complete":"lengkap","partial":"sebagian","unavailable":"tidak tersedia"}},"history":{"title":"Riwayat Echo","empty":"Belum ada bukti Echo","header":"# tick jenis status bukti ringkasan"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Cari","placeholder":"Filter berkas…","empty":"Tidak ada hasil","preview_label":"Pratinjau","preview_loading":"Memuat pratinjau…","preview_binary":"Berkas biner — pratinjau tidak tersedia","preview_unavailable":"Pratinjau tidak tersedia","preview_truncated":"Pratinjau dipotong"}};
-export const tr: TranslationSchema = {"footer":{"mode":{"browse":"gözat","insert":"ekle","normal":"normal","preview":"önizleme","settings":"ayarlar","files":"dosyalar","graft":"graft","history":"geçmiş"},"context":{"settings":"ayarlar","graft_empty":"incelemek için dosya aç","history_empty":"henüz Echo kanıtı yok","history_count":"Echo kanıtı: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k taşı","enter_change":"enter değiştir","f2_close":"f2 kapat","esc_close":"esc kapat","j_k_scroll":"j/k kaydır","f3_source":"f3 kaynak","f3_preview":"f3 önizleme","text_input":"metin girişi","esc_normal":"esc normal","ctrl_s_save":"ctrl+s kaydet","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab odak","tab_indent":"tab girinti","ctrl_b_files":"ctrl+b dosyalar","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h geçmiş","enter_open":"enter aç","backspace_up":"backspace yukarı","ctrl_b_close":"ctrl+b kapat","enter_jump":"enter git","r_refresh":"r yenile","ctrl_g_close":"ctrl+g kapat","ctrl_h_close":"ctrl+h kapat","i_insert":"i ekle","o_open_line":"o satır aç","cc_line":"cc satır","cw_word":"cw kelime","ce_word_end":"ce kelime sonu","c0_start":"c0 başlangıç","c_end":"c$ son","dd_line":"dd satır","dw_word":"dw kelime","de_word_end":"de kelime sonu","d0_start":"d0 başlangıç","d_end":"d$ son","yy_line":"yy satır","yw_word":"yw kelime","ye_word_end":"ye kelime sonu","y0_start":"y0 başlangıç","y_end":"y$ son","gg_top":"gg üst","esc_cancel":"esc iptal"},"evidence":{"head":"baş","tick":"tik","anchor":"çapa","coverage":"kapsam","complete":"tam","partial":"kısmi","unavailable":"kullanılamıyor"}},"history":{"title":"Echo Geçmişi","empty":"Henüz Echo kanıtı yok","header":"# tick tür durum kanıt özet"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const tr: TranslationSchema = {"footer":{"mode":{"browse":"gözat","insert":"ekle","normal":"normal","preview":"önizleme","settings":"ayarlar","files":"dosyalar","graft":"graft","history":"geçmiş"},"context":{"settings":"ayarlar","graft_empty":"incelemek için dosya aç","history_empty":"henüz Echo kanıtı yok","history_count":"Echo kanıtı: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k taşı","enter_change":"enter değiştir","f2_close":"f2 kapat","esc_close":"esc kapat","j_k_scroll":"j/k kaydır","f3_source":"f3 kaynak","f3_preview":"f3 önizleme","text_input":"metin girişi","esc_normal":"esc normal","ctrl_s_save":"ctrl+s kaydet","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab odak","tab_indent":"tab girinti","ctrl_b_files":"ctrl+b dosyalar","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h geçmiş","enter_open":"enter aç","backspace_up":"backspace yukarı","ctrl_b_close":"ctrl+b kapat","enter_jump":"enter git","r_refresh":"r yenile","ctrl_g_close":"ctrl+g kapat","ctrl_h_close":"ctrl+h kapat","i_insert":"i ekle","o_open_line":"o satır aç","cc_line":"cc satır","cw_word":"cw kelime","ce_word_end":"ce kelime sonu","c0_start":"c0 başlangıç","c_end":"c$ son","dd_line":"dd satır","dw_word":"dw kelime","de_word_end":"de kelime sonu","d0_start":"d0 başlangıç","d_end":"d$ son","yy_line":"yy satır","yw_word":"yw kelime","ye_word_end":"ye kelime sonu","y0_start":"y0 başlangıç","y_end":"y$ son","gg_top":"gg üst","esc_cancel":"esc iptal","type_filter":"filtre yaz","arrows_select":"↑/↓ seç","slash_filter":"/ filtre"},"evidence":{"head":"baş","tick":"tik","anchor":"çapa","coverage":"kapsam","complete":"tam","partial":"kısmi","unavailable":"kullanılamıyor"}},"history":{"title":"Echo Geçmişi","empty":"Henüz Echo kanıtı yok","header":"# tick tür durum kanıt özet"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Ara","placeholder":"Dosyaları filtrele…","empty":"Eşleşme yok","preview_label":"Önizleme","preview_loading":"Önizleme yükleniyor…","preview_binary":"İkili dosya — önizleme yok","preview_unavailable":"Önizleme kullanılamıyor","preview_truncated":"Önizleme kısaltıldı"}};
-export const hi: TranslationSchema = {"footer":{"mode":{"browse":"ब्राउज़","insert":"सम्मिलित","normal":"सामान्य","preview":"पूर्वावलोकन","settings":"सेटिंग","files":"फाइलें","graft":"Graft","history":"इतिहास"},"context":{"settings":"सेटिंग","graft_empty":"जांचने के लिए फाइल खोलें","history_empty":"अभी कोई Echo प्रमाण नहीं","history_count":"Echo प्रमाण: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k चलें","enter_change":"enter बदलें","f2_close":"f2 बंद","esc_close":"esc बंद","j_k_scroll":"j/k स्क्रोल","f3_source":"f3 स्रोत","f3_preview":"f3 पूर्वावलोकन","text_input":"पाठ इनपुट","esc_normal":"esc सामान्य","ctrl_s_save":"ctrl+s सहेजें","ctrl_t_theme":"ctrl+t थीम","tab_focus":"tab फोकस","tab_indent":"tab इंडेंट","ctrl_b_files":"ctrl+b फाइलें","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h इतिहास","enter_open":"enter खोलें","backspace_up":"backspace ऊपर","ctrl_b_close":"ctrl+b बंद","enter_jump":"enter जाएं","r_refresh":"r ताज़ा","ctrl_g_close":"ctrl+g बंद","ctrl_h_close":"ctrl+h बंद","i_insert":"i सम्मिलित","o_open_line":"o पंक्ति खोलें","cc_line":"cc पंक्ति","cw_word":"cw शब्द","ce_word_end":"ce शब्द अंत","c0_start":"c0 शुरुआत","c_end":"c$ अंत","dd_line":"dd पंक्ति","dw_word":"dw शब्द","de_word_end":"de शब्द अंत","d0_start":"d0 शुरुआत","d_end":"d$ अंत","yy_line":"yy पंक्ति","yw_word":"yw शब्द","ye_word_end":"ye शब्द अंत","y0_start":"y0 शुरुआत","y_end":"y$ अंत","gg_top":"gg ऊपर","esc_cancel":"esc रद्द"},"evidence":{"head":"शीर्ष","tick":"टिक","anchor":"एंकर","coverage":"कवरेज","complete":"पूर्ण","partial":"आंशिक","unavailable":"अनुपलब्ध"}},"history":{"title":"Echo इतिहास","empty":"अभी कोई Echo प्रमाण नहीं","header":"# tick प्रकार स्थिति प्रमाण सारांश"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const hi: TranslationSchema = {"footer":{"mode":{"browse":"ब्राउज़","insert":"सम्मिलित","normal":"सामान्य","preview":"पूर्वावलोकन","settings":"सेटिंग","files":"फाइलें","graft":"Graft","history":"इतिहास"},"context":{"settings":"सेटिंग","graft_empty":"जांचने के लिए फाइल खोलें","history_empty":"अभी कोई Echo प्रमाण नहीं","history_count":"Echo प्रमाण: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k चलें","enter_change":"enter बदलें","f2_close":"f2 बंद","esc_close":"esc बंद","j_k_scroll":"j/k स्क्रोल","f3_source":"f3 स्रोत","f3_preview":"f3 पूर्वावलोकन","text_input":"पाठ इनपुट","esc_normal":"esc सामान्य","ctrl_s_save":"ctrl+s सहेजें","ctrl_t_theme":"ctrl+t थीम","tab_focus":"tab फोकस","tab_indent":"tab इंडेंट","ctrl_b_files":"ctrl+b फाइलें","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h इतिहास","enter_open":"enter खोलें","backspace_up":"backspace ऊपर","ctrl_b_close":"ctrl+b बंद","enter_jump":"enter जाएं","r_refresh":"r ताज़ा","ctrl_g_close":"ctrl+g बंद","ctrl_h_close":"ctrl+h बंद","i_insert":"i सम्मिलित","o_open_line":"o पंक्ति खोलें","cc_line":"cc पंक्ति","cw_word":"cw शब्द","ce_word_end":"ce शब्द अंत","c0_start":"c0 शुरुआत","c_end":"c$ अंत","dd_line":"dd पंक्ति","dw_word":"dw शब्द","de_word_end":"de शब्द अंत","d0_start":"d0 शुरुआत","d_end":"d$ अंत","yy_line":"yy पंक्ति","yw_word":"yw शब्द","ye_word_end":"ye शब्द अंत","y0_start":"y0 शुरुआत","y_end":"y$ अंत","gg_top":"gg ऊपर","esc_cancel":"esc रद्द","type_filter":"फ़िल्टर लिखें","arrows_select":"↑/↓ चुनें","slash_filter":"/ फ़िल्टर"},"evidence":{"head":"शीर्ष","tick":"टिक","anchor":"एंकर","coverage":"कवरेज","complete":"पूर्ण","partial":"आंशिक","unavailable":"अनुपलब्ध"}},"history":{"title":"Echo इतिहास","empty":"अभी कोई Echo प्रमाण नहीं","header":"# tick प्रकार स्थिति प्रमाण सारांश"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"खोज","placeholder":"फ़ाइलें छाँटें…","empty":"कोई मेल नहीं","preview_label":"पूर्वावलोकन","preview_loading":"पूर्वावलोकन लोड हो रहा है…","preview_binary":"बाइनरी फ़ाइल — पूर्वावलोकन उपलब्ध नहीं","preview_unavailable":"पूर्वावलोकन उपलब्ध नहीं","preview_truncated":"पूर्वावलोकन संक्षिप्त है"}};
-export const nl: TranslationSchema = {"footer":{"mode":{"browse":"bladeren","insert":"invoegen","normal":"normaal","preview":"voorbeeld","settings":"instellingen","files":"bestanden","graft":"graft","history":"geschiedenis"},"context":{"settings":"instellingen","graft_empty":"open een bestand om te inspecteren","history_empty":"nog geen Echo-bewijs","history_count":"Echo-bewijs: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k bewegen","enter_change":"enter wijzigen","f2_close":"f2 sluiten","esc_close":"esc sluiten","j_k_scroll":"j/k scrollen","f3_source":"f3 bron","f3_preview":"f3 voorbeeld","text_input":"tekstinvoer","esc_normal":"esc normaal","ctrl_s_save":"ctrl+s opslaan","ctrl_t_theme":"ctrl+t thema","tab_focus":"tab focus","tab_indent":"tab inspringen","ctrl_b_files":"ctrl+b bestanden","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h geschiedenis","enter_open":"enter openen","backspace_up":"backspace omhoog","ctrl_b_close":"ctrl+b sluiten","enter_jump":"enter springen","r_refresh":"r vernieuwen","ctrl_g_close":"ctrl+g sluiten","ctrl_h_close":"ctrl+h sluiten","i_insert":"i invoegen","o_open_line":"o regel openen","cc_line":"cc regel","cw_word":"cw woord","ce_word_end":"ce woord-einde","c0_start":"c0 begin","c_end":"c$ einde","dd_line":"dd regel","dw_word":"dw woord","de_word_end":"de woord-einde","d0_start":"d0 begin","d_end":"d$ einde","yy_line":"yy regel","yw_word":"yw woord","ye_word_end":"ye woord-einde","y0_start":"y0 begin","y_end":"y$ einde","gg_top":"gg boven","esc_cancel":"esc annuleren"},"evidence":{"head":"kop","tick":"tick","anchor":"anker","coverage":"dekking","complete":"volledig","partial":"gedeeltelijk","unavailable":"niet beschikbaar"}},"history":{"title":"Echo-geschiedenis","empty":"Nog geen Echo-bewijs","header":"# tick soort status bewijs samenvatting"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const nl: TranslationSchema = {"footer":{"mode":{"browse":"bladeren","insert":"invoegen","normal":"normaal","preview":"voorbeeld","settings":"instellingen","files":"bestanden","graft":"graft","history":"geschiedenis"},"context":{"settings":"instellingen","graft_empty":"open een bestand om te inspecteren","history_empty":"nog geen Echo-bewijs","history_count":"Echo-bewijs: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k bewegen","enter_change":"enter wijzigen","f2_close":"f2 sluiten","esc_close":"esc sluiten","j_k_scroll":"j/k scrollen","f3_source":"f3 bron","f3_preview":"f3 voorbeeld","text_input":"tekstinvoer","esc_normal":"esc normaal","ctrl_s_save":"ctrl+s opslaan","ctrl_t_theme":"ctrl+t thema","tab_focus":"tab focus","tab_indent":"tab inspringen","ctrl_b_files":"ctrl+b bestanden","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h geschiedenis","enter_open":"enter openen","backspace_up":"backspace omhoog","ctrl_b_close":"ctrl+b sluiten","enter_jump":"enter springen","r_refresh":"r vernieuwen","ctrl_g_close":"ctrl+g sluiten","ctrl_h_close":"ctrl+h sluiten","i_insert":"i invoegen","o_open_line":"o regel openen","cc_line":"cc regel","cw_word":"cw woord","ce_word_end":"ce woord-einde","c0_start":"c0 begin","c_end":"c$ einde","dd_line":"dd regel","dw_word":"dw woord","de_word_end":"de woord-einde","d0_start":"d0 begin","d_end":"d$ einde","yy_line":"yy regel","yw_word":"yw woord","ye_word_end":"ye woord-einde","y0_start":"y0 begin","y_end":"y$ einde","gg_top":"gg boven","esc_cancel":"esc annuleren","type_filter":"filter typen","arrows_select":"↑/↓ selecteren","slash_filter":"/ filter"},"evidence":{"head":"kop","tick":"tick","anchor":"anker","coverage":"dekking","complete":"volledig","partial":"gedeeltelijk","unavailable":"niet beschikbaar"}},"history":{"title":"Echo-geschiedenis","empty":"Nog geen Echo-bewijs","header":"# tick soort status bewijs samenvatting"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Zoeken","placeholder":"Bestanden filteren…","empty":"Geen resultaten","preview_label":"Voorbeeld","preview_loading":"Voorbeeld laden…","preview_binary":"Binair bestand — geen voorbeeld","preview_unavailable":"Voorbeeld niet beschikbaar","preview_truncated":"Voorbeeld ingekort"}};
-export const pl: TranslationSchema = {"footer":{"mode":{"browse":"przeglądaj","insert":"wstaw","normal":"normalny","preview":"podgląd","settings":"ustawienia","files":"pliki","graft":"graft","history":"historia"},"context":{"settings":"ustawienia","graft_empty":"otwórz plik do sprawdzenia","history_empty":"brak jeszcze dowodów Echo","history_count":"Dowody Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k przesuń","enter_change":"enter zmień","f2_close":"f2 zamknij","esc_close":"esc zamknij","j_k_scroll":"j/k przewiń","f3_source":"f3 źródło","f3_preview":"f3 podgląd","text_input":"wpisywanie tekstu","esc_normal":"esc normalny","ctrl_s_save":"ctrl+s zapisz","ctrl_t_theme":"ctrl+t motyw","tab_focus":"tab fokus","tab_indent":"tab wcięcie","ctrl_b_files":"ctrl+b pliki","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historia","enter_open":"enter otwórz","backspace_up":"backspace w górę","ctrl_b_close":"ctrl+b zamknij","enter_jump":"enter skocz","r_refresh":"r odśwież","ctrl_g_close":"ctrl+g zamknij","ctrl_h_close":"ctrl+h zamknij","i_insert":"i wstaw","o_open_line":"o otwórz linię","cc_line":"cc linia","cw_word":"cw słowo","ce_word_end":"ce koniec słowa","c0_start":"c0 początek","c_end":"c$ koniec","dd_line":"dd linia","dw_word":"dw słowo","de_word_end":"de koniec słowa","d0_start":"d0 początek","d_end":"d$ koniec","yy_line":"yy linia","yw_word":"yw słowo","ye_word_end":"ye koniec słowa","y0_start":"y0 początek","y_end":"y$ koniec","gg_top":"gg góra","esc_cancel":"esc anuluj"},"evidence":{"head":"głowa","tick":"takt","anchor":"kotwica","coverage":"pokrycie","complete":"pełne","partial":"częściowe","unavailable":"niedostępne"}},"history":{"title":"Historia Echo","empty":"Brak jeszcze dowodów Echo","header":"# tick typ status dowód podsumowanie"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const pl: TranslationSchema = {"footer":{"mode":{"browse":"przeglądaj","insert":"wstaw","normal":"normalny","preview":"podgląd","settings":"ustawienia","files":"pliki","graft":"graft","history":"historia"},"context":{"settings":"ustawienia","graft_empty":"otwórz plik do sprawdzenia","history_empty":"brak jeszcze dowodów Echo","history_count":"Dowody Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k przesuń","enter_change":"enter zmień","f2_close":"f2 zamknij","esc_close":"esc zamknij","j_k_scroll":"j/k przewiń","f3_source":"f3 źródło","f3_preview":"f3 podgląd","text_input":"wpisywanie tekstu","esc_normal":"esc normalny","ctrl_s_save":"ctrl+s zapisz","ctrl_t_theme":"ctrl+t motyw","tab_focus":"tab fokus","tab_indent":"tab wcięcie","ctrl_b_files":"ctrl+b pliki","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historia","enter_open":"enter otwórz","backspace_up":"backspace w górę","ctrl_b_close":"ctrl+b zamknij","enter_jump":"enter skocz","r_refresh":"r odśwież","ctrl_g_close":"ctrl+g zamknij","ctrl_h_close":"ctrl+h zamknij","i_insert":"i wstaw","o_open_line":"o otwórz linię","cc_line":"cc linia","cw_word":"cw słowo","ce_word_end":"ce koniec słowa","c0_start":"c0 początek","c_end":"c$ koniec","dd_line":"dd linia","dw_word":"dw słowo","de_word_end":"de koniec słowa","d0_start":"d0 początek","d_end":"d$ koniec","yy_line":"yy linia","yw_word":"yw słowo","ye_word_end":"ye koniec słowa","y0_start":"y0 początek","y_end":"y$ koniec","gg_top":"gg góra","esc_cancel":"esc anuluj","type_filter":"wpisz filtr","arrows_select":"↑/↓ wybierz","slash_filter":"/ filtr"},"evidence":{"head":"głowa","tick":"takt","anchor":"kotwica","coverage":"pokrycie","complete":"pełne","partial":"częściowe","unavailable":"niedostępne"}},"history":{"title":"Historia Echo","empty":"Brak jeszcze dowodów Echo","header":"# tick typ status dowód podsumowanie"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Szukaj","placeholder":"Filtruj pliki…","empty":"Brak wyników","preview_label":"Podgląd","preview_loading":"Wczytywanie podglądu…","preview_binary":"Plik binarny — brak podglądu","preview_unavailable":"Podgląd niedostępny","preview_truncated":"Podgląd skrócony"}};
-export const vi: TranslationSchema = {"footer":{"mode":{"browse":"duyệt","insert":"chèn","normal":"thường","preview":"xem trước","settings":"cài đặt","files":"tệp","graft":"graft","history":"lịch sử"},"context":{"settings":"cài đặt","graft_empty":"mở tệp để kiểm tra","history_empty":"chưa có bằng chứng Echo","history_count":"Bằng chứng Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k di chuyển","enter_change":"enter đổi","f2_close":"f2 đóng","esc_close":"esc đóng","j_k_scroll":"j/k cuộn","f3_source":"f3 nguồn","f3_preview":"f3 xem trước","text_input":"nhập văn bản","esc_normal":"esc thường","ctrl_s_save":"ctrl+s lưu","ctrl_t_theme":"ctrl+t giao diện","tab_focus":"tab tiêu điểm","tab_indent":"tab thụt lề","ctrl_b_files":"ctrl+b tệp","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h lịch sử","enter_open":"enter mở","backspace_up":"backspace lên","ctrl_b_close":"ctrl+b đóng","enter_jump":"enter nhảy","r_refresh":"r làm mới","ctrl_g_close":"ctrl+g đóng","ctrl_h_close":"ctrl+h đóng","i_insert":"i chèn","o_open_line":"o mở dòng","cc_line":"cc dòng","cw_word":"cw từ","ce_word_end":"ce cuối từ","c0_start":"c0 đầu","c_end":"c$ cuối","dd_line":"dd dòng","dw_word":"dw từ","de_word_end":"de cuối từ","d0_start":"d0 đầu","d_end":"d$ cuối","yy_line":"yy dòng","yw_word":"yw từ","ye_word_end":"ye cuối từ","y0_start":"y0 đầu","y_end":"y$ cuối","gg_top":"gg đầu","esc_cancel":"esc hủy"},"evidence":{"head":"đầu","tick":"nhịp","anchor":"neo","coverage":"độ phủ","complete":"đầy đủ","partial":"một phần","unavailable":"không khả dụng"}},"history":{"title":"Lịch sử Echo","empty":"Chưa có bằng chứng Echo","header":"# tick loại trạng thái bằng chứng tóm tắt"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const vi: TranslationSchema = {"footer":{"mode":{"browse":"duyệt","insert":"chèn","normal":"thường","preview":"xem trước","settings":"cài đặt","files":"tệp","graft":"graft","history":"lịch sử"},"context":{"settings":"cài đặt","graft_empty":"mở tệp để kiểm tra","history_empty":"chưa có bằng chứng Echo","history_count":"Bằng chứng Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k di chuyển","enter_change":"enter đổi","f2_close":"f2 đóng","esc_close":"esc đóng","j_k_scroll":"j/k cuộn","f3_source":"f3 nguồn","f3_preview":"f3 xem trước","text_input":"nhập văn bản","esc_normal":"esc thường","ctrl_s_save":"ctrl+s lưu","ctrl_t_theme":"ctrl+t giao diện","tab_focus":"tab tiêu điểm","tab_indent":"tab thụt lề","ctrl_b_files":"ctrl+b tệp","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h lịch sử","enter_open":"enter mở","backspace_up":"backspace lên","ctrl_b_close":"ctrl+b đóng","enter_jump":"enter nhảy","r_refresh":"r làm mới","ctrl_g_close":"ctrl+g đóng","ctrl_h_close":"ctrl+h đóng","i_insert":"i chèn","o_open_line":"o mở dòng","cc_line":"cc dòng","cw_word":"cw từ","ce_word_end":"ce cuối từ","c0_start":"c0 đầu","c_end":"c$ cuối","dd_line":"dd dòng","dw_word":"dw từ","de_word_end":"de cuối từ","d0_start":"d0 đầu","d_end":"d$ cuối","yy_line":"yy dòng","yw_word":"yw từ","ye_word_end":"ye cuối từ","y0_start":"y0 đầu","y_end":"y$ cuối","gg_top":"gg đầu","esc_cancel":"esc hủy","type_filter":"nhập bộ lọc","arrows_select":"↑/↓ chọn","slash_filter":"/ lọc"},"evidence":{"head":"đầu","tick":"nhịp","anchor":"neo","coverage":"độ phủ","complete":"đầy đủ","partial":"một phần","unavailable":"không khả dụng"}},"history":{"title":"Lịch sử Echo","empty":"Chưa có bằng chứng Echo","header":"# tick loại trạng thái bằng chứng tóm tắt"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Tìm","placeholder":"Lọc tệp…","empty":"Không khớp","preview_label":"Xem trước","preview_loading":"Đang tải bản xem trước…","preview_binary":"Tệp nhị phân — không thể xem trước","preview_unavailable":"Không thể xem trước","preview_truncated":"Bản xem trước bị cắt"}};
-export const th: TranslationSchema = {"footer":{"mode":{"browse":"เรียกดู","insert":"แทรก","normal":"ปกติ","preview":"ดูตัวอย่าง","settings":"ตั้งค่า","files":"ไฟล์","graft":"Graft","history":"ประวัติ"},"context":{"settings":"ตั้งค่า","graft_empty":"เปิดไฟล์เพื่อตรวจสอบ","history_empty":"ยังไม่มีหลักฐาน Echo","history_count":"หลักฐาน Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k ย้าย","enter_change":"enter เปลี่ยน","f2_close":"f2 ปิด","esc_close":"esc ปิด","j_k_scroll":"j/k เลื่อน","f3_source":"f3 ต้นฉบับ","f3_preview":"f3 ดูตัวอย่าง","text_input":"ป้อนข้อความ","esc_normal":"esc ปกติ","ctrl_s_save":"ctrl+s บันทึก","ctrl_t_theme":"ctrl+t ธีม","tab_focus":"tab โฟกัส","tab_indent":"tab เยื้อง","ctrl_b_files":"ctrl+b ไฟล์","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h ประวัติ","enter_open":"enter เปิด","backspace_up":"backspace ขึ้น","ctrl_b_close":"ctrl+b ปิด","enter_jump":"enter ไป","r_refresh":"r รีเฟรช","ctrl_g_close":"ctrl+g ปิด","ctrl_h_close":"ctrl+h ปิด","i_insert":"i แทรก","o_open_line":"o เปิดบรรทัด","cc_line":"cc บรรทัด","cw_word":"cw คำ","ce_word_end":"ce จบคำ","c0_start":"c0 เริ่ม","c_end":"c$ จบ","dd_line":"dd บรรทัด","dw_word":"dw คำ","de_word_end":"de จบคำ","d0_start":"d0 เริ่ม","d_end":"d$ จบ","yy_line":"yy บรรทัด","yw_word":"yw คำ","ye_word_end":"ye จบคำ","y0_start":"y0 เริ่ม","y_end":"y$ จบ","gg_top":"gg บน","esc_cancel":"esc ยกเลิก"},"evidence":{"head":"เฮด","tick":"ทิก","anchor":"จุดยึด","coverage":"ความครอบคลุม","complete":"ครบถ้วน","partial":"บางส่วน","unavailable":"ไม่พร้อมใช้งาน"}},"history":{"title":"ประวัติ Echo","empty":"ยังไม่มีหลักฐาน Echo","header":"# tick ชนิด สถานะ หลักฐาน สรุป"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const th: TranslationSchema = {"footer":{"mode":{"browse":"เรียกดู","insert":"แทรก","normal":"ปกติ","preview":"ดูตัวอย่าง","settings":"ตั้งค่า","files":"ไฟล์","graft":"Graft","history":"ประวัติ"},"context":{"settings":"ตั้งค่า","graft_empty":"เปิดไฟล์เพื่อตรวจสอบ","history_empty":"ยังไม่มีหลักฐาน Echo","history_count":"หลักฐาน Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k ย้าย","enter_change":"enter เปลี่ยน","f2_close":"f2 ปิด","esc_close":"esc ปิด","j_k_scroll":"j/k เลื่อน","f3_source":"f3 ต้นฉบับ","f3_preview":"f3 ดูตัวอย่าง","text_input":"ป้อนข้อความ","esc_normal":"esc ปกติ","ctrl_s_save":"ctrl+s บันทึก","ctrl_t_theme":"ctrl+t ธีม","tab_focus":"tab โฟกัส","tab_indent":"tab เยื้อง","ctrl_b_files":"ctrl+b ไฟล์","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h ประวัติ","enter_open":"enter เปิด","backspace_up":"backspace ขึ้น","ctrl_b_close":"ctrl+b ปิด","enter_jump":"enter ไป","r_refresh":"r รีเฟรช","ctrl_g_close":"ctrl+g ปิด","ctrl_h_close":"ctrl+h ปิด","i_insert":"i แทรก","o_open_line":"o เปิดบรรทัด","cc_line":"cc บรรทัด","cw_word":"cw คำ","ce_word_end":"ce จบคำ","c0_start":"c0 เริ่ม","c_end":"c$ จบ","dd_line":"dd บรรทัด","dw_word":"dw คำ","de_word_end":"de จบคำ","d0_start":"d0 เริ่ม","d_end":"d$ จบ","yy_line":"yy บรรทัด","yw_word":"yw คำ","ye_word_end":"ye จบคำ","y0_start":"y0 เริ่ม","y_end":"y$ จบ","gg_top":"gg บน","esc_cancel":"esc ยกเลิก","type_filter":"พิมพ์ตัวกรอง","arrows_select":"↑/↓ เลือก","slash_filter":"/ กรอง"},"evidence":{"head":"เฮด","tick":"ทิก","anchor":"จุดยึด","coverage":"ความครอบคลุม","complete":"ครบถ้วน","partial":"บางส่วน","unavailable":"ไม่พร้อมใช้งาน"}},"history":{"title":"ประวัติ Echo","empty":"ยังไม่มีหลักฐาน Echo","header":"# tick ชนิด สถานะ หลักฐาน สรุป"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"ค้นหา","placeholder":"กรองไฟล์…","empty":"ไม่พบผลลัพธ์","preview_label":"ตัวอย่าง","preview_loading":"กำลังโหลดตัวอย่าง…","preview_binary":"ไฟล์ไบนารี — ไม่สามารถแสดงตัวอย่าง","preview_unavailable":"ไม่สามารถแสดงตัวอย่าง","preview_truncated":"ตัวอย่างถูกตัด"}};
-export const fa: TranslationSchema = {"footer":{"mode":{"browse":"مرور","insert":"درج","normal":"عادی","preview":"پیشنمایش","settings":"تنظیمات","files":"فایلها","graft":"Graft","history":"تاریخچه"},"context":{"settings":"تنظیمات","graft_empty":"برای بررسی یک فایل باز کنید","history_empty":"هنوز مدرک Echo وجود ندارد","history_count":"مدارک Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k حرکت","enter_change":"enter تغییر","f2_close":"f2 بستن","esc_close":"esc بستن","j_k_scroll":"j/k پیمایش","f3_source":"f3 منبع","f3_preview":"f3 پیشنمایش","text_input":"ورودی متن","esc_normal":"esc عادی","ctrl_s_save":"ctrl+s ذخیره","ctrl_t_theme":"ctrl+t پوسته","tab_focus":"tab تمرکز","tab_indent":"tab تورفتگی","ctrl_b_files":"ctrl+b فایلها","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h تاریخچه","enter_open":"enter باز کردن","backspace_up":"backspace بالا","ctrl_b_close":"ctrl+b بستن","enter_jump":"enter پرش","r_refresh":"r تازهسازی","ctrl_g_close":"ctrl+g بستن","ctrl_h_close":"ctrl+h بستن","i_insert":"i درج","o_open_line":"o باز کردن خط","cc_line":"cc خط","cw_word":"cw واژه","ce_word_end":"ce پایان واژه","c0_start":"c0 آغاز","c_end":"c$ پایان","dd_line":"dd خط","dw_word":"dw واژه","de_word_end":"de پایان واژه","d0_start":"d0 آغاز","d_end":"d$ پایان","yy_line":"yy خط","yw_word":"yw واژه","ye_word_end":"ye پایان واژه","y0_start":"y0 آغاز","y_end":"y$ پایان","gg_top":"gg بالا","esc_cancel":"esc لغو"},"evidence":{"head":"سر","tick":"تیک","anchor":"لنگر","coverage":"پوشش","complete":"کامل","partial":"جزئی","unavailable":"در دسترس نیست"}},"history":{"title":"تاریخچه Echo","empty":"هنوز مدرک Echo وجود ندارد","header":"# tick نوع وضعیت مدرک خلاصه"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const fa: TranslationSchema = {"footer":{"mode":{"browse":"مرور","insert":"درج","normal":"عادی","preview":"پیشنمایش","settings":"تنظیمات","files":"فایلها","graft":"Graft","history":"تاریخچه"},"context":{"settings":"تنظیمات","graft_empty":"برای بررسی یک فایل باز کنید","history_empty":"هنوز مدرک Echo وجود ندارد","history_count":"مدارک Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k حرکت","enter_change":"enter تغییر","f2_close":"f2 بستن","esc_close":"esc بستن","j_k_scroll":"j/k پیمایش","f3_source":"f3 منبع","f3_preview":"f3 پیشنمایش","text_input":"ورودی متن","esc_normal":"esc عادی","ctrl_s_save":"ctrl+s ذخیره","ctrl_t_theme":"ctrl+t پوسته","tab_focus":"tab تمرکز","tab_indent":"tab تورفتگی","ctrl_b_files":"ctrl+b فایلها","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h تاریخچه","enter_open":"enter باز کردن","backspace_up":"backspace بالا","ctrl_b_close":"ctrl+b بستن","enter_jump":"enter پرش","r_refresh":"r تازهسازی","ctrl_g_close":"ctrl+g بستن","ctrl_h_close":"ctrl+h بستن","i_insert":"i درج","o_open_line":"o باز کردن خط","cc_line":"cc خط","cw_word":"cw واژه","ce_word_end":"ce پایان واژه","c0_start":"c0 آغاز","c_end":"c$ پایان","dd_line":"dd خط","dw_word":"dw واژه","de_word_end":"de پایان واژه","d0_start":"d0 آغاز","d_end":"d$ پایان","yy_line":"yy خط","yw_word":"yw واژه","ye_word_end":"ye پایان واژه","y0_start":"y0 آغاز","y_end":"y$ پایان","gg_top":"gg بالا","esc_cancel":"esc لغو","type_filter":"نوشتن فیلتر","arrows_select":"↑/↓ انتخاب","slash_filter":"/ فیلتر"},"evidence":{"head":"سر","tick":"تیک","anchor":"لنگر","coverage":"پوشش","complete":"کامل","partial":"جزئی","unavailable":"در دسترس نیست"}},"history":{"title":"تاریخچه Echo","empty":"هنوز مدرک Echo وجود ندارد","header":"# tick نوع وضعیت مدرک خلاصه"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"جستجو","placeholder":"فیلتر پروندهها…","empty":"بدون نتیجه","preview_label":"پیشنمایش","preview_loading":"در حال بارگذاری پیشنمایش…","preview_binary":"فایل دودویی — پیشنمایش در دسترس نیست","preview_unavailable":"پیشنمایش در دسترس نیست","preview_truncated":"پیشنمایش کوتاه شده"}};
-export const he: TranslationSchema = {"footer":{"mode":{"browse":"עיון","insert":"הוספה","normal":"רגיל","preview":"תצוגה","settings":"הגדרות","files":"קבצים","graft":"Graft","history":"היסטוריה"},"context":{"settings":"הגדרות","graft_empty":"פתח קובץ לבדיקה","history_empty":"עדיין אין ראיות Echo","history_count":"ראיות Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k הזז","enter_change":"enter שנה","f2_close":"f2 סגור","esc_close":"esc סגור","j_k_scroll":"j/k גלול","f3_source":"f3 מקור","f3_preview":"f3 תצוגה","text_input":"קלט טקסט","esc_normal":"esc רגיל","ctrl_s_save":"ctrl+s שמור","ctrl_t_theme":"ctrl+t ערכת נושא","tab_focus":"tab מיקוד","tab_indent":"tab הזחה","ctrl_b_files":"ctrl+b קבצים","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h היסטוריה","enter_open":"enter פתח","backspace_up":"backspace למעלה","ctrl_b_close":"ctrl+b סגור","enter_jump":"enter קפוץ","r_refresh":"r רענן","ctrl_g_close":"ctrl+g סגור","ctrl_h_close":"ctrl+h סגור","i_insert":"i הוסף","o_open_line":"o פתח שורה","cc_line":"cc שורה","cw_word":"cw מילה","ce_word_end":"ce סוף מילה","c0_start":"c0 התחלה","c_end":"c$ סוף","dd_line":"dd שורה","dw_word":"dw מילה","de_word_end":"de סוף מילה","d0_start":"d0 התחלה","d_end":"d$ סוף","yy_line":"yy שורה","yw_word":"yw מילה","ye_word_end":"ye סוף מילה","y0_start":"y0 התחלה","y_end":"y$ סוף","gg_top":"gg למעלה","esc_cancel":"esc בטל"},"evidence":{"head":"ראש","tick":"טיק","anchor":"עוגן","coverage":"כיסוי","complete":"מלא","partial":"חלקי","unavailable":"לא זמין"}},"history":{"title":"היסטוריית Echo","empty":"עדיין אין ראיות Echo","header":"# tick סוג מצב ראיה סיכום"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}};
+export const he: TranslationSchema = {"footer":{"mode":{"browse":"עיון","insert":"הוספה","normal":"רגיל","preview":"תצוגה","settings":"הגדרות","files":"קבצים","graft":"Graft","history":"היסטוריה"},"context":{"settings":"הגדרות","graft_empty":"פתח קובץ לבדיקה","history_empty":"עדיין אין ראיות Echo","history_count":"ראיות Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k הזז","enter_change":"enter שנה","f2_close":"f2 סגור","esc_close":"esc סגור","j_k_scroll":"j/k גלול","f3_source":"f3 מקור","f3_preview":"f3 תצוגה","text_input":"קלט טקסט","esc_normal":"esc רגיל","ctrl_s_save":"ctrl+s שמור","ctrl_t_theme":"ctrl+t ערכת נושא","tab_focus":"tab מיקוד","tab_indent":"tab הזחה","ctrl_b_files":"ctrl+b קבצים","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h היסטוריה","enter_open":"enter פתח","backspace_up":"backspace למעלה","ctrl_b_close":"ctrl+b סגור","enter_jump":"enter קפוץ","r_refresh":"r רענן","ctrl_g_close":"ctrl+g סגור","ctrl_h_close":"ctrl+h סגור","i_insert":"i הוסף","o_open_line":"o פתח שורה","cc_line":"cc שורה","cw_word":"cw מילה","ce_word_end":"ce סוף מילה","c0_start":"c0 התחלה","c_end":"c$ סוף","dd_line":"dd שורה","dw_word":"dw מילה","de_word_end":"de סוף מילה","d0_start":"d0 התחלה","d_end":"d$ סוף","yy_line":"yy שורה","yw_word":"yw מילה","ye_word_end":"ye סוף מילה","y0_start":"y0 התחלה","y_end":"y$ סוף","gg_top":"gg למעלה","esc_cancel":"esc בטל","type_filter":"הקלדת סינון","arrows_select":"↑/↓ בחירה","slash_filter":"/ סינון"},"evidence":{"head":"ראש","tick":"טיק","anchor":"עוגן","coverage":"כיסוי","complete":"מלא","partial":"חלקי","unavailable":"לא זמין"}},"history":{"title":"היסטוריית Echo","empty":"עדיין אין ראיות Echo","header":"# tick סוג מצב ראיה סיכום"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"חיפוש","placeholder":"סינון קבצים…","empty":"אין התאמות","preview_label":"תצוגה מקדימה","preview_loading":"טוען תצוגה מקדימה…","preview_binary":"קובץ בינארי — אין תצוגה מקדימה","preview_unavailable":"תצוגה מקדימה אינה זמינה","preview_truncated":"תצוגה מקדימה מקוצרת"}};
-export const me: TranslationSchema = {"footer":{"mode":{"browse":"esworb","insert":"tresni","normal":"lamron","preview":"weiverp","settings":"sgnittes","files":"selif","graft":"tfarg","history":"yrotsih"},"context":{"settings":"sgnittes","graft_empty":"ti tcepsni ot elif a nepo","history_empty":"tey ecnedive ohcE on","history_count":"{count} :ecnedive ohcE"},"command":{"invalid":"pleh rof pleh: epyt ;dezingocer ton dnammoC","hints":{"tab_accept":"tpecca bat","enter_run":"nur retne","esc_cancel":"lecnac cse"},"details":{"edit":"elif a nepO","write":"elif tnerruc eht etirW","quit":"tidej tiuQ","wq":"tiuq dna etirW","ttd":"daeh lacinonac gnivom tuohtiw kcit lasuac a evresbO","strand":"sdnarts etirw-no-ypoc tsil ro ,hctiws ,etaerC","braid":"setadidnac diarb timda ro ,weiverp ,weiV","why":"dnammoc lufgninaem tsal eht nialpxE","help":"pleh dnammoc wohS"}},"hints":{"j_k_move":"evom k/j","enter_change":"egnahc retne","f2_close":"esolc 2f","esc_close":"esolc cse","j_k_scroll":"llorcs k/j","f3_source":"ecruos 3f","f3_preview":"weiverp 3f","text_input":"tupni txet","esc_normal":"lamron cse","ctrl_s_save":"evas s+lrtc","ctrl_t_theme":"emeht t+lrtc","tab_focus":"sucof bat","tab_indent":"tnedni bat","ctrl_b_files":"selif b+lrtc","ctrl_g_graft":"tfarg g+lrtc","ctrl_h_history":"yrotsih h+lrtc","enter_open":"nepo retne","backspace_up":"pu ecapskcab","ctrl_b_close":"esolc b+lrtc","enter_jump":"pmuj retne","r_refresh":"hserfer r","ctrl_g_close":"esolc g+lrtc","ctrl_h_close":"esolc h+lrtc","i_insert":"tresni i","o_open_line":"enil nepo o","cc_line":"enil cc","cw_word":"drow wc","ce_word_end":"dne-drow ec","c0_start":"trats 0c","c_end":"dne $c","dd_line":"enil dd","dw_word":"drow wd","de_word_end":"dne-drow ed","d0_start":"trats 0d","d_end":"dne $d","yy_line":"enil yy","yw_word":"drow wy","ye_word_end":"dne-drow ey","y0_start":"trats 0y","y_end":"dne $y","gg_top":"pot gg","esc_cancel":"lecnac cse"},"evidence":{"head":"daeh","tick":"kcit","anchor":"rohcna","coverage":"egarevoc","complete":"etelpmoc","partial":"laitrap","unavailable":"elbaliavanu"}},"history":{"title":"yrotsiH ohcE","empty":"tey ecnedive ohcE oN","header":"yrammus ecnedive sutats dnik kcit #"},"startupFileModal":{"title":"elif nepO","current_directory":"yrotcerid tnerruC","empty":"yrotcerid siht ni selif oN"},"settings":{"sections":{"appearance":"ecnaraeppA","editor":"rotidE","runtime":"emitnuR"},"rows":{"theme":{"label":"emehT","description":".semeht nevird-atad dellatsni neewteb hctiwS"},"theme_mode":{"label":"krad/thgiL","description":".noinapmoc krad ro thgil sti ot emeht tnerruc eht hctiwS"},"footer":{"label":"retooF","description":".egde mottob eht ta stnih dnammoc dna ,sucof ,edom wohS"},"markdown_preview":{"label":"weiverp nwodkraM","description":".weiverp dna ecruos neewteb reffub nwodkraM evitca eht hctiwS"},"diagnostics":{"label":"scitsongaiD","description":".gniriw emitnur lufroloC dna ,resrap ,tfarG tcepsnI"},"line_numbers":{"label":"srebmun eniL","description":".srebmun enil rotide evitaler-rosruc dna etulosba neewteb hctiwS"},"gutter_dimmed":{"label":"rettug miD","description":".tes nekot rettug demmid emeht eht esU"},"causal_gutter_basis":{"label":"srekram lasuaC","description":".srekram egnahc rettug yb desu sisab lasuac eht esoohC"}},"values":{"on":"nO","off":"ffO","theme_mode_dark":"kraD","theme_mode_light":"thgiL","line_numbers_absolute":"etulosbA","line_numbers_relative":"evitaleR","source":"ecruoS","preview":"weiverP","current":"tnerruC","open":"nepO","causal_gutter_last_save":"evas tsaL","causal_gutter_import":"tropmI","causal_gutter_selected_checkpoint":"tniopkcehc detceleS","causal_gutter_selected_tick":"kcit detceleS"},"toast":{"changed_title":"degnahc sgnitteS"}},"worldline":{"title":"senildlroW","empty":"tey senildlrow oN","header":"tcilfnoc atled daeh sisab eman dnik"},"why":{"range_obstructed_title":"detcurtsbo egnar yhW"}};
+export const me: TranslationSchema = {"footer":{"mode":{"browse":"esworb","insert":"tresni","normal":"lamron","preview":"weiverp","settings":"sgnittes","files":"selif","graft":"tfarg","history":"yrotsih"},"context":{"settings":"sgnittes","graft_empty":"ti tcepsni ot elif a nepo","history_empty":"tey ecnedive ohcE on","history_count":"{count} :ecnedive ohcE"},"command":{"invalid":"pleh rof pleh: epyt ;dezingocer ton dnammoC","hints":{"tab_accept":"tpecca bat","enter_run":"nur retne","esc_cancel":"lecnac cse"},"details":{"edit":"elif a nepO","write":"elif tnerruc eht etirW","quit":"tidej tiuQ","wq":"tiuq dna etirW","ttd":"daeh lacinonac gnivom tuohtiw kcit lasuac a evresbO","strand":"sdnarts etirw-no-ypoc tsil ro ,hctiws ,etaerC","braid":"setadidnac diarb timda ro ,weiverp ,weiV","why":"dnammoc lufgninaem tsal eht nialpxE","help":"pleh dnammoc wohS"}},"hints":{"j_k_move":"evom k/j","enter_change":"egnahc retne","f2_close":"esolc 2f","esc_close":"esolc cse","j_k_scroll":"llorcs k/j","f3_source":"ecruos 3f","f3_preview":"weiverp 3f","text_input":"tupni txet","esc_normal":"lamron cse","ctrl_s_save":"evas s+lrtc","ctrl_t_theme":"emeht t+lrtc","tab_focus":"sucof bat","tab_indent":"tnedni bat","ctrl_b_files":"selif b+lrtc","ctrl_g_graft":"tfarg g+lrtc","ctrl_h_history":"yrotsih h+lrtc","enter_open":"nepo retne","backspace_up":"pu ecapskcab","ctrl_b_close":"esolc b+lrtc","enter_jump":"pmuj retne","r_refresh":"hserfer r","ctrl_g_close":"esolc g+lrtc","ctrl_h_close":"esolc h+lrtc","i_insert":"tresni i","o_open_line":"enil nepo o","cc_line":"enil cc","cw_word":"drow wc","ce_word_end":"dne-drow ec","c0_start":"trats 0c","c_end":"dne $c","dd_line":"enil dd","dw_word":"drow wd","de_word_end":"dne-drow ed","d0_start":"trats 0d","d_end":"dne $d","yy_line":"enil yy","yw_word":"drow wy","ye_word_end":"dne-drow ey","y0_start":"trats 0y","y_end":"dne $y","gg_top":"pot gg","esc_cancel":"lecnac cse","type_filter":"retlif epyt","arrows_select":"tceles ↓/↑","slash_filter":"retlif /"},"evidence":{"head":"daeh","tick":"kcit","anchor":"rohcna","coverage":"egarevoc","complete":"etelpmoc","partial":"laitrap","unavailable":"elbaliavanu"}},"history":{"title":"yrotsiH ohcE","empty":"tey ecnedive ohcE oN","header":"yrammus ecnedive sutats dnik kcit #"},"startupFileModal":{"title":"elif nepO","current_directory":"yrotcerid tnerruC","empty":"yrotcerid siht ni selif oN"},"settings":{"sections":{"appearance":"ecnaraeppA","editor":"rotidE","runtime":"emitnuR"},"rows":{"theme":{"label":"emehT","description":".semeht nevird-atad dellatsni neewteb hctiwS"},"theme_mode":{"label":"krad/thgiL","description":".noinapmoc krad ro thgil sti ot emeht tnerruc eht hctiwS"},"footer":{"label":"retooF","description":".egde mottob eht ta stnih dnammoc dna ,sucof ,edom wohS"},"markdown_preview":{"label":"weiverp nwodkraM","description":".weiverp dna ecruos neewteb reffub nwodkraM evitca eht hctiwS"},"diagnostics":{"label":"scitsongaiD","description":".gniriw emitnur lufroloC dna ,resrap ,tfarG tcepsnI"},"line_numbers":{"label":"srebmun eniL","description":".srebmun enil rotide evitaler-rosruc dna etulosba neewteb hctiwS"},"gutter_dimmed":{"label":"rettug miD","description":".tes nekot rettug demmid emeht eht esU"},"causal_gutter_basis":{"label":"srekram lasuaC","description":".srekram egnahc rettug yb desu sisab lasuac eht esoohC"}},"values":{"on":"nO","off":"ffO","theme_mode_dark":"kraD","theme_mode_light":"thgiL","line_numbers_absolute":"etulosbA","line_numbers_relative":"evitaleR","source":"ecruoS","preview":"weiverP","current":"tnerruC","open":"nepO","causal_gutter_last_save":"evas tsaL","causal_gutter_import":"tropmI","causal_gutter_selected_checkpoint":"tniopkcehc detceleS","causal_gutter_selected_tick":"kcit detceleS"},"toast":{"changed_title":"degnahc sgnitteS"}},"worldline":{"title":"senildlroW","empty":"tey senildlrow oN","header":"tcilfnoc atled daeh sisab eman dnik"},"why":{"range_obstructed_title":"detcurtsbo egnar yhW"},"fileExplorer":{"search_label":"hcraeS","placeholder":"…selif retliF","empty":"sehctam oN","preview_label":"weiverP","preview_loading":"…weiverp gnidaoL","preview_binary":"elbaliavanu weiverp — elif yraniB","preview_unavailable":"elbaliavanu weiverP","preview_truncated":"detacnurt weiverP"}};
export const locales = ["en", "fr", "it", "de", "es", "ko", "ja", "zh-Hans", "pt-BR", "ru", "ar", "id", "tr", "hi", "nl", "pl", "vi", "th", "fa", "he", "me"] as const;
diff --git a/src/main-workspace.ts b/src/main-workspace.ts
index 0875cf2b..24732887 100644
--- a/src/main-workspace.ts
+++ b/src/main-workspace.ts
@@ -1,3 +1,5 @@
+import { getDefaultContext } from '@flyingrobots/bijou';
+import { workspaceTerminalIO } from './adapters/workspace-terminal-io.js';
import { initDefaultContext } from '@flyingrobots/bijou-node';
import { run } from '@flyingrobots/bijou-tui';
import { createWorkspaceApp } from './adapters/workspace-app.js';
@@ -72,7 +74,8 @@ export async function runJeditWorkspace(): Promise {
initialWorkingDirectory: DEFAULT_WORKING_DIRECTORY,
...workspaceInstrumentationFromEnv(process.env),
}, productionText);
- await run(app, { mouse: JEDIT_TERMINAL_MOUSE_OPTIONS.mouse });
+ const context = getDefaultContext();
+ await run(app, { ...JEDIT_TERMINAL_MOUSE_OPTIONS, ctx: { ...context, io: workspaceTerminalIO(context.io) } });
});
}
diff --git a/src/ports/file-preview.ts b/src/ports/file-preview.ts
new file mode 100644
index 00000000..bdc15d8d
--- /dev/null
+++ b/src/ports/file-preview.ts
@@ -0,0 +1,12 @@
+export const FilePreviewKinds = Object.freeze({ Loaded: 'loaded', Binary: 'binary', Unavailable: 'unavailable' } as const);
+export const FILE_PREVIEW_MAX_BYTES = 65_536;
+export const FILE_PREVIEW_MAX_LINES = 200;
+
+export type FilePreviewResult =
+ | { readonly kind: typeof FilePreviewKinds.Loaded; readonly lines: readonly string[]; readonly truncated: boolean }
+ | { readonly kind: typeof FilePreviewKinds.Binary }
+ | { readonly kind: typeof FilePreviewKinds.Unavailable };
+
+export interface FilePreviewSource {
+ readonly loadFilePreview: (path: string) => FilePreviewResult;
+}
diff --git a/src/ports/file-system.ts b/src/ports/file-system.ts
index f29ebf6d..138577bd 100644
--- a/src/ports/file-system.ts
+++ b/src/ports/file-system.ts
@@ -15,6 +15,7 @@ export interface FileEntry {
readonly kind: FileEntryKind;
readonly name: string;
readonly path: string;
+ readonly searchPath?: string;
}
export interface DirectoryIssue {
diff --git a/src/ui/file-search-input.ts b/src/ui/file-search-input.ts
new file mode 100644
index 00000000..74605548
--- /dev/null
+++ b/src/ui/file-search-input.ts
@@ -0,0 +1,87 @@
+import { createSurface, stringToSurface, type Surface } from '@flyingrobots/bijou';
+import { clipToWidth } from '@flyingrobots/bijou-tui';
+import type { FileSearchState } from '../app/workspace/file-search.js';
+import type { JeditTheme, JeditStyleToken } from './jedit-theme.js';
+import { fitLine, visibleLineLength } from './fit-line.js';
+
+const INPUT_HEIGHT = 1;
+const MIN_INPUT_WIDTH = 1;
+const EMPTY_INPUT = '';
+const LABEL_SUFFIX = ': ';
+const CHARACTER_STEP = 1;
+
+export interface FileSearchInputOptions {
+ readonly state: FileSearchState;
+ readonly label: string;
+ readonly placeholder: string;
+ readonly focused: boolean;
+ readonly width: number;
+ readonly theme: JeditTheme;
+}
+
+interface FileSearchInputWindow {
+ readonly prefix: string;
+ readonly prefixWidth: number;
+ readonly text: string;
+ readonly startIndex: number;
+ readonly cursorColumn: number;
+}
+
+export function renderFileSearchInput(options: FileSearchInputOptions): Surface {
+ if (options.width <= 0) return createSurface(0, INPUT_HEIGHT);
+ const window = inputWindow(options);
+ const content = options.state.input.length === 0
+ ? clipToWidth(options.placeholder, options.width - window.prefixWidth)
+ : window.text;
+ const surface = stringToSurface(fitLine(`${window.prefix}${content}`, options.width), options.width, INPUT_HEIGHT);
+ applyInputToken(surface, fileSearchInputToken(options.theme));
+ if (options.focused && window.cursorColumn < options.width) {
+ const cell = surface.get(window.cursorColumn, 0);
+ surface.set(window.cursorColumn, 0, { ...cell, ...inputCellStyle(options.theme.cursor.insert), empty: false });
+ }
+ return surface;
+}
+
+function inputWindow(options: FileSearchInputOptions): FileSearchInputWindow {
+ const prefix = clipToWidth(`${options.label}${LABEL_SUFFIX}`, Math.max(0, options.width - MIN_INPUT_WIDTH));
+ const prefixWidth = visibleLineLength(prefix);
+ const capacity = Math.max(MIN_INPUT_WIDTH, options.width - prefixWidth);
+ const characters = Array.from(options.state.input);
+ const cursor = Math.max(0, Math.min(characters.length, options.state.cursorIndex));
+ let startIndex = 0;
+ while (startIndex < cursor && visibleLineLength(characters.slice(startIndex, cursor).join(EMPTY_INPUT)) >= capacity) {
+ startIndex += CHARACTER_STEP;
+ }
+ return {
+ prefix, prefixWidth, startIndex,
+ text: clipToWidth(characters.slice(startIndex).join(EMPTY_INPUT), capacity),
+ cursorColumn: prefixWidth + visibleLineLength(characters.slice(startIndex, cursor).join(EMPTY_INPUT)),
+ };
+}
+
+export function fileSearchCursorAtColumn(options: FileSearchInputOptions, column: number): number {
+ const window = inputWindow(options);
+ const characters = Array.from(options.state.input);
+ let position = window.prefixWidth;
+ for (let index = window.startIndex; index < characters.length; index += CHARACTER_STEP) {
+ if (column <= position) return index;
+ position += visibleLineLength(characters[index] ?? EMPTY_INPUT);
+ }
+ return characters.length;
+}
+
+function applyInputToken(surface: Surface, token: JeditStyleToken): void {
+ for (let x = 0; x < surface.width; x += CHARACTER_STEP) {
+ surface.set(x, 0, { ...surface.get(x, 0), ...inputCellStyle(token), empty: false });
+ }
+}
+
+function inputCellStyle(token: JeditStyleToken) {
+ return { fg: token.fg, fgRGB: token.fgRGB, bg: token.bg, bgRGB: token.bgRGB, modifiers: token.modifiers == null ? undefined : [...token.modifiers] };
+}
+
+function fileSearchInputToken(theme: JeditTheme): JeditStyleToken {
+ return theme.surface.currentLine.bg === theme.surface.drawer.bg
+ ? theme.surface.workspace
+ : theme.surface.currentLine;
+}
diff --git a/src/ui/file-search-results.ts b/src/ui/file-search-results.ts
new file mode 100644
index 00000000..956c2405
--- /dev/null
+++ b/src/ui/file-search-results.ts
@@ -0,0 +1,60 @@
+import type { Surface } from '@flyingrobots/bijou';
+import type { FileSearchEntry } from '../app/workspace/file-search.js';
+import { legibleOn } from './theme-contrast.js';
+import type { JeditTheme, JeditStyleToken } from './jedit-theme.js';
+import { JEDIT_TEXT_MODIFIER } from './jedit-theme.js';
+import { visibleLineLength } from './fit-line.js';
+
+const ENTRY_PREFIX_WIDTH = 2;
+const CHARACTER_STEP = 1;
+const MIN_SELECTION_CONTRAST = 4.5;
+const HEX_RADIX = 16;
+const HEX_PAIR_WIDTH = 2;
+
+export function highlightFileSearchMatches(surface: Surface, entries: readonly FileSearchEntry[], theme: JeditTheme, selection: FileSearchSelection): void {
+ entries.forEach(({ entry, matchIndexes, index: sourceIndex }, row) => {
+ const selected = sourceIndex === selection.selectedIndex;
+ paintFileSearchRow(surface, row, theme, { selected, hovered: sourceIndex === selection.hoveredIndex });
+ const characters = Array.from(entry.name);
+ for (const index of matchIndexes) {
+ const column = ENTRY_PREFIX_WIDTH + visibleLineLength(characters.slice(0, index).join(''));
+ const width = visibleLineLength(characters[index] ?? '');
+ for (let x = column; x < Math.min(surface.width, column + width); x += CHARACTER_STEP) {
+ const cell = surface.get(x, row);
+ surface.set(x, row, {
+ ...cell, fg: selected ? cell.fg : theme.chrome.activeEdge.fg,
+ fgRGB: selected ? cell.fgRGB : theme.chrome.activeEdge.fgRGB,
+ modifiers: [...(cell.modifiers ?? []), JEDIT_TEXT_MODIFIER.Bold],
+ });
+ }
+ }
+ });
+}
+
+export interface FileSearchSelection {
+ readonly selectedIndex: number;
+ readonly hoveredIndex?: number;
+}
+
+interface FileSearchRowState {
+ readonly selected: boolean;
+ readonly hovered: boolean;
+}
+
+function paintFileSearchRow(surface: Surface, row: number, theme: JeditTheme, state: FileSearchRowState): void {
+ if (!state.selected && !state.hovered) return;
+ const token = state.selected ? fileSelectionToken(theme) : theme.surface.currentLine;
+ for (let x = 0; x < surface.width; x += CHARACTER_STEP) {
+ const cell = surface.get(x, row);
+ surface.set(x, row, { ...cell, fg: token.fg, fgRGB: token.fgRGB, bg: token.bg, bgRGB: token.bgRGB });
+ }
+}
+
+function fileSelectionToken(theme: JeditTheme): JeditStyleToken {
+ const background = theme.cursor.normal.bgRGB;
+ const foreground = theme.surface.workspace.bgRGB;
+ if (background == null || foreground == null) return theme.cursor.normal;
+ const fgRGB = legibleOn(foreground, [background], MIN_SELECTION_CONTRAST);
+ const fg = `#${fgRGB.map((channel) => channel.toString(HEX_RADIX).padStart(HEX_PAIR_WIDTH, '0')).join('')}`;
+ return { ...theme.cursor.normal, fg, fgRGB };
+}
diff --git a/src/ui/terminal-mouse.ts b/src/ui/terminal-mouse.ts
index d73490a9..2eab6a5f 100644
--- a/src/ui/terminal-mouse.ts
+++ b/src/ui/terminal-mouse.ts
@@ -1,3 +1,4 @@
export const JEDIT_TERMINAL_MOUSE_OPTIONS = {
mouse: true,
+ mouseMode: 'any',
} as const;
diff --git a/src/ui/workspace-chrome.ts b/src/ui/workspace-chrome.ts
index 2c3307a2..24eb4663 100644
--- a/src/ui/workspace-chrome.ts
+++ b/src/ui/workspace-chrome.ts
@@ -40,6 +40,9 @@ const EditorModeKeys: Record = Object.freeze({
});
const FooterHintKeys = Object.freeze({
+ TypeFilter: 'type_filter',
+ SlashFilter: 'slash_filter',
+ ArrowsSelect: 'arrows_select',
JkMove: 'j_k_move',
EnterChange: 'enter_change',
F2Close: 'f2_close',
@@ -94,6 +97,7 @@ export interface WorkspaceFooterState {
readonly i18n: I18nPort;
readonly focusPane: FocusPane;
readonly fileDrawerOpen: boolean;
+ readonly fileSearchEditing?: boolean;
readonly graftDrawerOpen: boolean;
readonly viewMode: ViewMode;
readonly markdownPreviewActive: boolean;
@@ -322,9 +326,9 @@ function previewFooterHints(state: WorkspaceFooterState, t: FooterHintTranslator
function fileDrawerFooterHints(state: WorkspaceFooterState, t: FooterHintTranslator): ReadonlyArray {
return [
- t(FooterHintKeys.JkMove),
- t(FooterHintKeys.EnterOpen),
- t(FooterHintKeys.BackspaceUp),
+ t(state.fileSearchEditing ? FooterHintKeys.TypeFilter : FooterHintKeys.JkMove),
+ t(state.fileSearchEditing ? FooterHintKeys.EscNormal : FooterHintKeys.SlashFilter),
+ state.fileSearchEditing ? undefined : t(FooterHintKeys.EnterOpen),
t(FooterHintKeys.CtrlBClose),
focusHint(state, t),
];
diff --git a/translations.json b/translations.json
index d63a553f..e7060a50 100644
--- a/translations.json
+++ b/translations.json
@@ -82,7 +82,10 @@
"y0_start": "y0 start",
"y_end": "y$ end",
"gg_top": "gg top",
- "esc_cancel": "esc cancel"
+ "esc_cancel": "esc cancel",
+ "type_filter": "type filter",
+ "arrows_select": "↑/↓ select",
+ "slash_filter": "/ filter"
},
"evidence": {
"head": "head",
@@ -171,6 +174,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Search",
+ "placeholder": "Filter files…",
+ "empty": "No matches",
+ "preview_label": "Preview",
+ "preview_loading": "Loading preview…",
+ "preview_binary": "Binary file — preview unavailable",
+ "preview_unavailable": "Preview unavailable",
+ "preview_truncated": "Preview truncated"
}
}
},
@@ -256,7 +269,10 @@
"y0_start": "y0 début",
"y_end": "y$ fin",
"gg_top": "gg haut",
- "esc_cancel": "esc annuler"
+ "esc_cancel": "esc annuler",
+ "type_filter": "saisir le filtre",
+ "arrows_select": "↑/↓ sélectionner",
+ "slash_filter": "/ filtrer"
},
"evidence": {
"head": "tête",
@@ -345,6 +361,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Recherche",
+ "placeholder": "Filtrer les fichiers…",
+ "empty": "Aucun résultat",
+ "preview_label": "Aperçu",
+ "preview_loading": "Chargement de l’aperçu…",
+ "preview_binary": "Fichier binaire — aperçu indisponible",
+ "preview_unavailable": "Aperçu indisponible",
+ "preview_truncated": "Aperçu tronqué"
}
}
},
@@ -430,7 +456,10 @@
"y0_start": "y0 inizio",
"y_end": "y$ fine",
"gg_top": "gg cima",
- "esc_cancel": "esc annulla"
+ "esc_cancel": "esc annulla",
+ "type_filter": "digita filtro",
+ "arrows_select": "↑/↓ seleziona",
+ "slash_filter": "/ filtra"
},
"evidence": {
"head": "testa",
@@ -519,6 +548,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Cerca",
+ "placeholder": "Filtra file…",
+ "empty": "Nessun risultato",
+ "preview_label": "Anteprima",
+ "preview_loading": "Caricamento anteprima…",
+ "preview_binary": "File binario — anteprima non disponibile",
+ "preview_unavailable": "Anteprima non disponibile",
+ "preview_truncated": "Anteprima troncata"
}
}
},
@@ -604,7 +643,10 @@
"y0_start": "y0 anfang",
"y_end": "y$ ende",
"gg_top": "gg oben",
- "esc_cancel": "esc abbrechen"
+ "esc_cancel": "esc abbrechen",
+ "type_filter": "Filter eingeben",
+ "arrows_select": "↑/↓ auswählen",
+ "slash_filter": "/ filtern"
},
"evidence": {
"head": "Kopf",
@@ -693,6 +735,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Suche",
+ "placeholder": "Dateien filtern…",
+ "empty": "Keine Treffer",
+ "preview_label": "Vorschau",
+ "preview_loading": "Vorschau wird geladen…",
+ "preview_binary": "Binärdatei — keine Vorschau",
+ "preview_unavailable": "Vorschau nicht verfügbar",
+ "preview_truncated": "Vorschau gekürzt"
}
}
},
@@ -778,7 +830,10 @@
"y0_start": "y0 inicio",
"y_end": "y$ fin",
"gg_top": "gg arriba",
- "esc_cancel": "esc cancelar"
+ "esc_cancel": "esc cancelar",
+ "type_filter": "escribir filtro",
+ "arrows_select": "↑/↓ seleccionar",
+ "slash_filter": "/ filtrar"
},
"evidence": {
"head": "cabeza",
@@ -867,6 +922,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Buscar",
+ "placeholder": "Filtrar archivos…",
+ "empty": "Sin resultados",
+ "preview_label": "Vista previa",
+ "preview_loading": "Cargando vista previa…",
+ "preview_binary": "Archivo binario — sin vista previa",
+ "preview_unavailable": "Vista previa no disponible",
+ "preview_truncated": "Vista previa truncada"
}
}
},
@@ -952,7 +1017,10 @@
"y0_start": "y0 시작",
"y_end": "y$ 끝",
"gg_top": "gg 맨 위",
- "esc_cancel": "esc 취소"
+ "esc_cancel": "esc 취소",
+ "type_filter": "필터 입력",
+ "arrows_select": "↑/↓ 선택",
+ "slash_filter": "/ 필터"
},
"evidence": {
"head": "헤드",
@@ -1041,6 +1109,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "검색",
+ "placeholder": "파일 필터…",
+ "empty": "일치 항목 없음",
+ "preview_label": "미리 보기",
+ "preview_loading": "미리 보기 불러오는 중…",
+ "preview_binary": "바이너리 파일 — 미리 보기 불가",
+ "preview_unavailable": "미리 보기 불가",
+ "preview_truncated": "미리 보기 생략됨"
}
}
},
@@ -1126,7 +1204,10 @@
"y0_start": "y0 先頭",
"y_end": "y$ 末尾",
"gg_top": "gg 先頭",
- "esc_cancel": "esc 取消"
+ "esc_cancel": "esc 取消",
+ "type_filter": "絞り込み入力",
+ "arrows_select": "↑/↓ 選択",
+ "slash_filter": "/ 絞り込み"
},
"evidence": {
"head": "ヘッド",
@@ -1215,6 +1296,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "検索",
+ "placeholder": "ファイルを絞り込む…",
+ "empty": "一致なし",
+ "preview_label": "プレビュー",
+ "preview_loading": "プレビューを読み込み中…",
+ "preview_binary": "バイナリファイル — プレビュー不可",
+ "preview_unavailable": "プレビュー不可",
+ "preview_truncated": "プレビューを省略"
}
}
},
@@ -1300,7 +1391,10 @@
"y0_start": "y0 开头",
"y_end": "y$ 结尾",
"gg_top": "gg 顶部",
- "esc_cancel": "esc 取消"
+ "esc_cancel": "esc 取消",
+ "type_filter": "输入筛选",
+ "arrows_select": "↑/↓ 选择",
+ "slash_filter": "/ 筛选"
},
"evidence": {
"head": "头",
@@ -1389,6 +1483,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "搜索",
+ "placeholder": "筛选文件…",
+ "empty": "无匹配项",
+ "preview_label": "预览",
+ "preview_loading": "正在加载预览…",
+ "preview_binary": "二进制文件 — 无法预览",
+ "preview_unavailable": "无法预览",
+ "preview_truncated": "预览已截断"
}
}
},
@@ -1474,7 +1578,10 @@
"y0_start": "y0 início",
"y_end": "y$ fim",
"gg_top": "gg topo",
- "esc_cancel": "esc cancelar"
+ "esc_cancel": "esc cancelar",
+ "type_filter": "digitar filtro",
+ "arrows_select": "↑/↓ selecionar",
+ "slash_filter": "/ filtrar"
},
"evidence": {
"head": "cabeça",
@@ -1563,6 +1670,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Buscar",
+ "placeholder": "Filtrar arquivos…",
+ "empty": "Nenhum resultado",
+ "preview_label": "Prévia",
+ "preview_loading": "Carregando prévia…",
+ "preview_binary": "Arquivo binário — sem prévia",
+ "preview_unavailable": "Prévia indisponível",
+ "preview_truncated": "Prévia truncada"
}
}
},
@@ -1648,7 +1765,10 @@
"y0_start": "y0 начало",
"y_end": "y$ конец",
"gg_top": "gg верх",
- "esc_cancel": "esc отмена"
+ "esc_cancel": "esc отмена",
+ "type_filter": "ввод фильтра",
+ "arrows_select": "↑/↓ выбор",
+ "slash_filter": "/ фильтр"
},
"evidence": {
"head": "вершина",
@@ -1737,6 +1857,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Поиск",
+ "placeholder": "Фильтр файлов…",
+ "empty": "Нет совпадений",
+ "preview_label": "Просмотр",
+ "preview_loading": "Загрузка просмотра…",
+ "preview_binary": "Двоичный файл — просмотр недоступен",
+ "preview_unavailable": "Просмотр недоступен",
+ "preview_truncated": "Просмотр сокращён"
}
}
},
@@ -1822,7 +1952,10 @@
"y0_start": "y0 بداية",
"y_end": "y$ نهاية",
"gg_top": "gg أعلى",
- "esc_cancel": "esc إلغاء"
+ "esc_cancel": "esc إلغاء",
+ "type_filter": "اكتب للتصفية",
+ "arrows_select": "↑/↓ تحديد",
+ "slash_filter": "/ تصفية"
},
"evidence": {
"head": "الرأس",
@@ -1911,6 +2044,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "بحث",
+ "placeholder": "تصفية الملفات…",
+ "empty": "لا نتائج",
+ "preview_label": "معاينة",
+ "preview_loading": "جارٍ تحميل المعاينة…",
+ "preview_binary": "ملف ثنائي — المعاينة غير متاحة",
+ "preview_unavailable": "المعاينة غير متاحة",
+ "preview_truncated": "المعاينة مختصرة"
}
}
},
@@ -1996,7 +2139,10 @@
"y0_start": "y0 awal",
"y_end": "y$ akhir",
"gg_top": "gg atas",
- "esc_cancel": "esc batal"
+ "esc_cancel": "esc batal",
+ "type_filter": "ketik filter",
+ "arrows_select": "↑/↓ pilih",
+ "slash_filter": "/ filter"
},
"evidence": {
"head": "kepala",
@@ -2085,6 +2231,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Cari",
+ "placeholder": "Filter berkas…",
+ "empty": "Tidak ada hasil",
+ "preview_label": "Pratinjau",
+ "preview_loading": "Memuat pratinjau…",
+ "preview_binary": "Berkas biner — pratinjau tidak tersedia",
+ "preview_unavailable": "Pratinjau tidak tersedia",
+ "preview_truncated": "Pratinjau dipotong"
}
}
},
@@ -2170,7 +2326,10 @@
"y0_start": "y0 başlangıç",
"y_end": "y$ son",
"gg_top": "gg üst",
- "esc_cancel": "esc iptal"
+ "esc_cancel": "esc iptal",
+ "type_filter": "filtre yaz",
+ "arrows_select": "↑/↓ seç",
+ "slash_filter": "/ filtre"
},
"evidence": {
"head": "baş",
@@ -2259,6 +2418,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Ara",
+ "placeholder": "Dosyaları filtrele…",
+ "empty": "Eşleşme yok",
+ "preview_label": "Önizleme",
+ "preview_loading": "Önizleme yükleniyor…",
+ "preview_binary": "İkili dosya — önizleme yok",
+ "preview_unavailable": "Önizleme kullanılamıyor",
+ "preview_truncated": "Önizleme kısaltıldı"
}
}
},
@@ -2344,7 +2513,10 @@
"y0_start": "y0 शुरुआत",
"y_end": "y$ अंत",
"gg_top": "gg ऊपर",
- "esc_cancel": "esc रद्द"
+ "esc_cancel": "esc रद्द",
+ "type_filter": "फ़िल्टर लिखें",
+ "arrows_select": "↑/↓ चुनें",
+ "slash_filter": "/ फ़िल्टर"
},
"evidence": {
"head": "शीर्ष",
@@ -2433,6 +2605,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "खोज",
+ "placeholder": "फ़ाइलें छाँटें…",
+ "empty": "कोई मेल नहीं",
+ "preview_label": "पूर्वावलोकन",
+ "preview_loading": "पूर्वावलोकन लोड हो रहा है…",
+ "preview_binary": "बाइनरी फ़ाइल — पूर्वावलोकन उपलब्ध नहीं",
+ "preview_unavailable": "पूर्वावलोकन उपलब्ध नहीं",
+ "preview_truncated": "पूर्वावलोकन संक्षिप्त है"
}
}
},
@@ -2518,7 +2700,10 @@
"y0_start": "y0 begin",
"y_end": "y$ einde",
"gg_top": "gg boven",
- "esc_cancel": "esc annuleren"
+ "esc_cancel": "esc annuleren",
+ "type_filter": "filter typen",
+ "arrows_select": "↑/↓ selecteren",
+ "slash_filter": "/ filter"
},
"evidence": {
"head": "kop",
@@ -2607,6 +2792,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Zoeken",
+ "placeholder": "Bestanden filteren…",
+ "empty": "Geen resultaten",
+ "preview_label": "Voorbeeld",
+ "preview_loading": "Voorbeeld laden…",
+ "preview_binary": "Binair bestand — geen voorbeeld",
+ "preview_unavailable": "Voorbeeld niet beschikbaar",
+ "preview_truncated": "Voorbeeld ingekort"
}
}
},
@@ -2692,7 +2887,10 @@
"y0_start": "y0 początek",
"y_end": "y$ koniec",
"gg_top": "gg góra",
- "esc_cancel": "esc anuluj"
+ "esc_cancel": "esc anuluj",
+ "type_filter": "wpisz filtr",
+ "arrows_select": "↑/↓ wybierz",
+ "slash_filter": "/ filtr"
},
"evidence": {
"head": "głowa",
@@ -2781,6 +2979,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Szukaj",
+ "placeholder": "Filtruj pliki…",
+ "empty": "Brak wyników",
+ "preview_label": "Podgląd",
+ "preview_loading": "Wczytywanie podglądu…",
+ "preview_binary": "Plik binarny — brak podglądu",
+ "preview_unavailable": "Podgląd niedostępny",
+ "preview_truncated": "Podgląd skrócony"
}
}
},
@@ -2866,7 +3074,10 @@
"y0_start": "y0 đầu",
"y_end": "y$ cuối",
"gg_top": "gg đầu",
- "esc_cancel": "esc hủy"
+ "esc_cancel": "esc hủy",
+ "type_filter": "nhập bộ lọc",
+ "arrows_select": "↑/↓ chọn",
+ "slash_filter": "/ lọc"
},
"evidence": {
"head": "đầu",
@@ -2955,6 +3166,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "Tìm",
+ "placeholder": "Lọc tệp…",
+ "empty": "Không khớp",
+ "preview_label": "Xem trước",
+ "preview_loading": "Đang tải bản xem trước…",
+ "preview_binary": "Tệp nhị phân — không thể xem trước",
+ "preview_unavailable": "Không thể xem trước",
+ "preview_truncated": "Bản xem trước bị cắt"
}
}
},
@@ -3040,7 +3261,10 @@
"y0_start": "y0 เริ่ม",
"y_end": "y$ จบ",
"gg_top": "gg บน",
- "esc_cancel": "esc ยกเลิก"
+ "esc_cancel": "esc ยกเลิก",
+ "type_filter": "พิมพ์ตัวกรอง",
+ "arrows_select": "↑/↓ เลือก",
+ "slash_filter": "/ กรอง"
},
"evidence": {
"head": "เฮด",
@@ -3129,6 +3353,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "ค้นหา",
+ "placeholder": "กรองไฟล์…",
+ "empty": "ไม่พบผลลัพธ์",
+ "preview_label": "ตัวอย่าง",
+ "preview_loading": "กำลังโหลดตัวอย่าง…",
+ "preview_binary": "ไฟล์ไบนารี — ไม่สามารถแสดงตัวอย่าง",
+ "preview_unavailable": "ไม่สามารถแสดงตัวอย่าง",
+ "preview_truncated": "ตัวอย่างถูกตัด"
}
}
},
@@ -3214,7 +3448,10 @@
"y0_start": "y0 آغاز",
"y_end": "y$ پایان",
"gg_top": "gg بالا",
- "esc_cancel": "esc لغو"
+ "esc_cancel": "esc لغو",
+ "type_filter": "نوشتن فیلتر",
+ "arrows_select": "↑/↓ انتخاب",
+ "slash_filter": "/ فیلتر"
},
"evidence": {
"head": "سر",
@@ -3303,6 +3540,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "جستجو",
+ "placeholder": "فیلتر پروندهها…",
+ "empty": "بدون نتیجه",
+ "preview_label": "پیشنمایش",
+ "preview_loading": "در حال بارگذاری پیشنمایش…",
+ "preview_binary": "فایل دودویی — پیشنمایش در دسترس نیست",
+ "preview_unavailable": "پیشنمایش در دسترس نیست",
+ "preview_truncated": "پیشنمایش کوتاه شده"
}
}
},
@@ -3388,7 +3635,10 @@
"y0_start": "y0 התחלה",
"y_end": "y$ סוף",
"gg_top": "gg למעלה",
- "esc_cancel": "esc בטל"
+ "esc_cancel": "esc בטל",
+ "type_filter": "הקלדת סינון",
+ "arrows_select": "↑/↓ בחירה",
+ "slash_filter": "/ סינון"
},
"evidence": {
"head": "ראש",
@@ -3477,6 +3727,16 @@
},
"why": {
"range_obstructed_title": "Why range obstructed"
+ },
+ "fileExplorer": {
+ "search_label": "חיפוש",
+ "placeholder": "סינון קבצים…",
+ "empty": "אין התאמות",
+ "preview_label": "תצוגה מקדימה",
+ "preview_loading": "טוען תצוגה מקדימה…",
+ "preview_binary": "קובץ בינארי — אין תצוגה מקדימה",
+ "preview_unavailable": "תצוגה מקדימה אינה זמינה",
+ "preview_truncated": "תצוגה מקדימה מקוצרת"
}
}
}