@narumitw/pi-files 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/LICENSE +21 -0
  3. package/README.md +102 -2
  4. package/dist/chunks/chunk-3GQ7VS22.ts +273 -0
  5. package/dist/chunks/chunk-3GQ7VS22.ts.map +7 -0
  6. package/dist/chunks/chunk-AXW73HDJ.ts +31 -0
  7. package/dist/chunks/chunk-AXW73HDJ.ts.map +7 -0
  8. package/dist/chunks/chunk-F5QFC255.ts +101 -0
  9. package/dist/chunks/chunk-F5QFC255.ts.map +7 -0
  10. package/dist/chunks/chunk-NFNGJETD.ts +77 -0
  11. package/dist/chunks/chunk-NFNGJETD.ts.map +7 -0
  12. package/dist/chunks/chunk-OPGTWNJI.ts +258 -0
  13. package/dist/chunks/chunk-OPGTWNJI.ts.map +7 -0
  14. package/dist/chunks/chunk-WOTDNRTG.ts +242 -0
  15. package/dist/chunks/chunk-WOTDNRTG.ts.map +7 -0
  16. package/dist/chunks/file-context-explorer-WR6GQZHF.ts +1793 -0
  17. package/dist/chunks/file-context-explorer-WR6GQZHF.ts.map +7 -0
  18. package/dist/chunks/file-context-menu-VPLHUUQ6.ts +302 -0
  19. package/dist/chunks/file-context-menu-VPLHUUQ6.ts.map +7 -0
  20. package/dist/chunks/files-menu-MES4MQ4F.ts +119 -0
  21. package/dist/chunks/files-menu-MES4MQ4F.ts.map +7 -0
  22. package/dist/chunks/files-settings-NYXQU2HV.ts +295 -0
  23. package/dist/chunks/files-settings-NYXQU2HV.ts.map +7 -0
  24. package/dist/chunks/menu-P6IBMROH.ts +876 -0
  25. package/dist/chunks/menu-P6IBMROH.ts.map +7 -0
  26. package/dist/index.ts +1789 -0
  27. package/dist/index.ts.map +7 -0
  28. package/docs/edit-scope-reference.md +142 -0
  29. package/docs/file-context-reference.md +91 -0
  30. package/docs/migration.md +105 -0
  31. package/docs/verification.md +99 -0
  32. package/package.json +58 -4
  33. package/src/content-search-session.ts +204 -0
  34. package/src/content-search-ui.ts +159 -0
  35. package/src/content-search.ts +170 -0
  36. package/src/context-service.ts +515 -0
  37. package/src/external-editor.ts +143 -0
  38. package/src/file-browser-ui.ts +128 -0
  39. package/src/file-browser.ts +76 -0
  40. package/src/file-context-explorer.ts +992 -0
  41. package/src/file-context-menu.ts +360 -0
  42. package/src/file-context-preview-ui.ts +354 -0
  43. package/src/file-context-settings.ts +301 -0
  44. package/src/file-context.ts +321 -0
  45. package/src/file-search.ts +246 -0
  46. package/src/files-menu.ts +125 -0
  47. package/src/files-settings.ts +314 -0
  48. package/src/files-widget.ts +65 -0
  49. package/src/files.ts +148 -0
  50. package/src/git-context.ts +485 -0
  51. package/src/index.ts +1 -0
  52. package/src/scope/contracts.ts +243 -0
  53. package/src/scope/display.ts +27 -0
  54. package/src/scope/edit-scope.ts +435 -0
  55. package/src/scope/files.ts +118 -0
  56. package/src/scope/input.ts +207 -0
  57. package/src/scope/menu.ts +277 -0
  58. package/src/scope/paths.ts +142 -0
  59. package/src/scope/picker-keys.ts +154 -0
  60. package/src/scope/picker.ts +484 -0
  61. package/src/scope/policy.ts +55 -0
  62. package/src/scope/settings.ts +138 -0
  63. package/src/scope/state.ts +91 -0
  64. package/src/scope/ui.ts +106 -0
@@ -0,0 +1,105 @@
1
+ # Migrate to Pi Files
2
+
3
+ Pi Files replaces the repository's File Context and Edit Scope implementations with one owner. **Never load `pi-files` together with `pi-file-context` or `pi-edit-scope`.** Duplicate aliases, shortcuts, and guard handlers are not a coordination mechanism; load order cannot make co-loading safe.
4
+
5
+ Repository retirement is approved. This does not authorize first publication, npm visibility changes, tags, release workflow dispatch, or npm deprecation notices. The read-only `@narumitw/pi-files` metadata query returned `E404` during implementation; Pi Files is not yet published. Use the local build until a release is explicitly approved.
6
+
7
+ ## Prepare
8
+
9
+ 1. Stop the current run and clear pending messages before changing permissions or resources. Record the complete scope target list and whether scope is active or blocked.
10
+ 2. Review the context queue. Submit or explicitly remove its snapshots before reload; they are intentionally in-memory only and are not migrated or restored from disk. Do not disable a restriction merely to perform migration.
11
+ 3. Record loaded extension sources in both Pi install scopes. Use `pi list` and inspect personal `settings.json` under `getAgentDir()` (normally `~/.pi/agent`) and trusted project `.pi/settings.json`. Also check explicit `-e` arguments, source paths, repository resource filters, and auto-discovered extension files.
12
+ 4. Preserve both existing user settings files and a copy of the verification session. Do not rename files, rewrite entry IDs, delete scope state, or remove notices to make recovery appear successful.
13
+
14
+ Extensions still run with full user permissions. The [native stock-tool boundary](../README.md#-security-and-privacy) is not expanded by the merge, and the user-controlled external editor remains outside the guard.
15
+
16
+ ## Replace installed resources
17
+
18
+ Personal and project managed npm roots are separate installation scopes. Deduplication within one root does not uninstall or disable another scope's resources. Different source identities, copied files, or explicit CLI resources can still expose a predecessor. A root manifest change cannot remove those installations.
19
+
20
+ Remove or disable every predecessor resource **before loading the successor**, then reload or restart once the resource set is coherent. Removal changes package/resource registration, not extension-owned settings or session state.
21
+
22
+ For npm sources matching these examples, remove personal registrations:
23
+
24
+ ```bash
25
+ pi remove npm:@narumitw/pi-file-context
26
+ pi remove npm:@narumitw/pi-edit-scope
27
+ ```
28
+
29
+ From each affected project, remove project registrations independently:
30
+
31
+ ```bash
32
+ pi remove --local npm:@narumitw/pi-file-context
33
+ pi remove --local npm:@narumitw/pi-edit-scope
34
+ ```
35
+
36
+ Use the exact recorded source if an installation uses another path, Git source, or version specification. Do not assume these example commands cover a local copy, a package resource filter, or an explicit `-e` argument. Inspect both scopes after removal.
37
+
38
+ If a predecessor must remain installed for reference, disable all its extension resources instead. For a package declaration, an `extensions: []` filter loads none of that package's extensions. Apply filtering in the scope that owns the declaration and preserve unrelated resources. Remove old entrypoint selections from Git-repository filters; add `packages/pi-files/src/index.ts` if selecting the successor. Filters only narrow declared resources, so an old pinned repository revision without the successor must be updated first.
39
+
40
+ Move auto-discovered predecessor extension files outside discovery roots, remove their explicit resource paths, or disable them through Pi's resource configuration. Remove predecessor `-e` arguments from launch scripts. Do not use duplicate command suffixes, shortcut precedence, or one blocking guard to hide a still-loaded predecessor.
41
+
42
+ The current repository root manifest replaces both predecessor source entries with `./packages/pi-files/src/index.ts`. The archived trees under `deprecated/` are references, excluded from active workspaces and the default root entrypoint list. Installing this repository still enables its other declared extensions unless filtered.
43
+
44
+ ## Build and load the local successor
45
+
46
+ From the repository root, with predecessor resources removed or disabled:
47
+
48
+ ```bash
49
+ npm install
50
+ npm run build
51
+ pi -e ./packages/pi-files
52
+ ```
53
+
54
+ The root build supplies Pi TUI Kit and generates the successor's `dist/index.ts`. Package-directory loading requires this build; its generated entry does not import back into `src`. Pi's bundled peers stay external. Local loading does not require a published Pi Files package, but dependency installation still requires registry access or an adequate npm cache; this is not a promise of an air-gapped install.
55
+
56
+ For later sessions, choose the intended registration scope:
57
+
58
+ 1. Personal installation, from the repository root:
59
+
60
+ ```bash
61
+ pi install ./packages/pi-files
62
+ ```
63
+
64
+ 2. Project installation instead, from each affected project directory:
65
+
66
+ ```bash
67
+ pi install --local /absolute/path/to/pi-extensions/packages/pi-files
68
+ ```
69
+
70
+ Replace the example path with the built checkout path. The current directory selects which project receives the registration; it does not need to be the checkout.
71
+
72
+ Choose one intended registration rather than loading the same checkout by several paths. Restart Pi or run `/reload` after changing resources. Settings and durable scope restore; queued context does not. Do not send built-in `/reload` as an RPC prompt command, where it can become model input.
73
+
74
+ ## What stays compatible
75
+
76
+ | Area | Preserved behavior |
77
+ | --- | --- |
78
+ | Commands | `/file-context [browse\|remove]` keeps the dedicated context workflow; `/edit-scope`, `/scope`, and their `off` routes keep the dedicated scope workflow. `/files` adds the common manager, `browse`, and `off`. |
79
+ | Context | Exact snapshots, order, limits, Git provenance, whole-file references, and one-time attachment retain their format. The `file-context-quotes` identity remains unchanged. Custom-triggered turns do not consume the queue. |
80
+ | Scope state | `scope-state`, `scope-contract`, and `scope-context-anchor`, snapshot version 1, original entry IDs, state references, and historical notice bytes remain unchanged. |
81
+ | Recovery | The active branch restores after reload, resume, fork, tree navigation, and compaction as applicable. Invalid or uncertain state stays blocked until repaired or explicitly ended. |
82
+ | Settings | `openShortcut` remains in `<getAgentDir()>/pi-file-context.json`; `showHiddenFiles` remains in `<getAgentDir()>/pi-scope.json`. Unknown fields and existing values are preserved. |
83
+ | Defaults | Browser shortcut is `ctrl+shift+x`; hidden scope discovery defaults to `false`. Settings remain user-only. |
84
+
85
+ The settings basenames and historical `Scope` / `/scope` notice text are deliberate compatibility exceptions. There is no `pi-files.json`, project settings migration, file renaming, or automatic legacy-file deletion. Do not copy values into Pi's core settings file. See [context settings](file-context-reference.md#settings) and [scope settings](edit-scope-reference.md#settings-and-persistence).
86
+
87
+ The new common Settings screen uses Pi's `SettingsList`. Only the reload-requiring shortcut action is blocked while context is queued; the hidden-file preference still applies immediately. Dedicated aliases keep their existing settings routes. A successful shortcut save remains saved if automatic reload fails; repair the reload problem and run `/reload` manually.
88
+
89
+ ## Verify the transition
90
+
91
+ 1. Confirm that only the successor owns these commands and the combined status widget. There must be no predecessor resource left loaded, even if no duplicate is visible.
92
+ 2. Open `/files`, `/file-context`, `/edit-scope`, and `/scope` in TUI mode. Check the common manager and both dedicated workflows without changing the allowed set. Review Status for the same branch and full targets recorded before migration.
93
+ 3. Confirm a restored active restriction still rejects unselected writes and unsupported tools. A blocked restriction must remain blocked; repair targets through scope management or explicitly end it only if that is the intended user action.
94
+ 4. Queue a disposable snapshot and confirm this does not authorize its path. Review or remove it without changing scope. Confirm applying or ending scope does not consume context.
95
+ 5. Check the two unchanged settings paths. Do not expect old queued snapshots to reappear after reload. Check current automated and manual evidence in [verification](verification.md).
96
+
97
+ Scope cancellation and run completion do not end restrictions. The explorer and checked-file picker keep distinct Tab, Enter, Escape, input, and paste behavior. There is no combined quote-and-authorize action. See the full [context reference](file-context-reference.md) and [scope reference](edit-scope-reference.md) for discovery, validation, security, and recovery limits.
98
+
99
+ ## Rollback and recovery
100
+
101
+ Stop the run before rollback. Submit or remove queued context if needed, record the loaded sources, remove or disable Pi Files, restore the predecessor resources together, and reload **without co-loading guards**. Build predecessor resources if their package entrypoints require it. Restore the coherent repository layout and root manifest together through Git recovery when rolling back a checkout.
102
+
103
+ Retained settings basenames, version-1 scope entries, original entry IDs, and historical notice bytes permit predecessor restoration without rewriting the session. Use copied sessions to verify forward and reverse recovery. Do not delete state, change IDs, or discard anchors to recover unrestricted access. A failed or uncertain scope publication stays blocked until the user repairs the target set or explicitly ends the restriction. If restoration differs from the recorded branch state, stop guarded work and inspect recovery rather than bypassing it.
104
+
105
+ Registry unpublication, visibility changes, npm deprecation, tags, releases, and release workflow dispatch are not rollback steps authorized by this merge.
@@ -0,0 +1,99 @@
1
+ # Pi Files verification
2
+
3
+ Pi Files has one extension factory and two independent domain services. The repository cutover replaces both predecessor entrypoints with `packages/pi-files/src/index.ts`; archived sources and tests remain under `deprecated/`. The maintainer authorized this retirement through the end-to-end implementation request. Publication and npm deprecation remain unauthorized.
4
+
5
+ ## Compatibility baseline
6
+
7
+ Baseline commit: `30ef6143`. Before moving sources, all 33 predecessor suites passed: 597 tests passed and one existing path test was skipped. Direct tests require canonical `TMPDIR` on macOS, as the root test runner already provides; otherwise `/var` versus `/private/var` fixture expectations differ. No production path behavior was changed to hide that fixture issue.
8
+
9
+ | Area | Preserved contract | Evidence |
10
+ | --- | --- | --- |
11
+ | Context | Ordered immutable snapshots; unchanged XML escaping, Git provenance, bounds, `file-context-quotes` identity, and custom-turn exclusion. | Migrated context, pending-quote, Git, search, preview, and external-editor suites. |
12
+ | Permissions | Context never grants permission; scope never loads content or consumes context. | `files-integration.test.ts` tests attachment, apply/off, tree restoration, and concurrent owners. |
13
+ | Durable scope | Snapshot version 1; `scope-state`, `scope-contract`, `scope-context-anchor`; original entry IDs and historical notice bytes. | `migration.test.ts` executes extracted predecessor pure readers and successor readers in both directions, including delayed publication and compaction recovery. |
14
+ | Lifecycle | Scope restores on start/tree; context resets on start/shutdown, not same-manager tree navigation. Restrictions survive run completion and cancellation. | Migrated scope runtime/lifecycle tests and merged integration tests. |
15
+ | Settings | User-only `pi-file-context.json` and `pi-scope.json`; unchanged fields/defaults; no canonical-file migration or automatic writes. | Existing store suites, generated-entry byte-preservation checks, and combined Settings tests. |
16
+ | UI | Dedicated context explorer and checked-file picker; existing navigation, editing, paste, custom bindings, and cancellation. | Migrated UI/picker/explorer suites plus native common Settings tests. |
17
+ | Loading | One factory, eager guard registration, lazy specialized UI, external Pi/Kit imports, exact generated paths. | Builder/eager-graph tests, generated-entry Jiti test, and clean production-install fixture. |
18
+
19
+ The source guidance to prefer direct user-owned context selection is retained in the successor's `AGENTS.md`. Existing changelogs remain in the archives. The pending predecessor breadth-first Changeset is retained in its archive's `release-notes/`; its behavior is included in the successor's release intent instead of targeting a removed workspace.
20
+
21
+ ## Decision branches and safety review
22
+
23
+ Tool-policy tests cover inactive, active, blocked, invalid, and uninitialized state; stock readers versus same-named replacements; selected versus unselected native writers; and shell, codemode, delegation, unknown tools, and `readOnlyHint` tools. Loadout tests cover the presence or absence of a stock direct writer and active native `codemode.mode=only`. The service does not change core settings or active tool definitions to make a loadout acceptable.
24
+
25
+ Path tests cover exact canonical targets, lexical aliases, symlink changes, hard links, existing files, reservations, removed targets, workspace mismatch, and validation interrupted by cancellation or scope replacement. Uncertain state publication remains blocked even when a session append changed memory before disk failure. Scope, contract, path, policy, and settings implementations retain the predecessor's formats and enforcement decisions.
26
+
27
+ The quote queue remains independent under ordinary/custom turns, duplicate-looking snapshots, limits, review/removal, scope apply/off, reload, tree navigation, and shutdown. Tests also cover two headless session managers sharing one no-op UI object. Missing or invalid context is not converted into edit authorization.
28
+
29
+ External editing remains user-controlled and outside native-tool enforcement. Project trust, contained regular worktree targets, final-symlink rejection, mutation-queue ownership, process cancellation, terminal restoration, and preview reload retain their previous checks. This is not an OS sandbox or an attestation of arbitrary SDK tool overrides. Historical revisions remain read-only.
30
+
31
+ ### Lifecycle and ownership
32
+
33
+ Services own resources by `sessionManager`. Each UI flow also has a cancellation controller; only one context, scope, or manager flow is active for an owner. Repeated context openings can join their existing flow. Other overlapping openings are rejected, and invalid arguments still receive their original usage error rather than joining an unrelated operation.
34
+
35
+ Lifecycle hooks abort the common flow without awaiting the reload-dependent command. Waiting for that command would deadlock because `ctx.reload()` itself waits for shutdown. Domain cleanup drains native tasks and settings writes instead. Both common Settings and the legacy context settings route have regression tests where reload invokes shutdown/start from inside the command while scope remains active.
36
+
37
+ Discovery and Git initialization promises are tracked separately, so one failure cannot hide a surviving sibling. Git contexts track their individual subprocess requests and drain them after explorer cancellation; initialization settles every read, and Git calls wait for child close before releasing ownership. Completed explorers release their Git context instead of retaining each project's status map for the session. File reads and external-editor processes also join the owner's native-work set. Consumer/widget errors do not skip cleanup. Held-task regression tests verify that shutdown waits for work to settle after cancellation or a sibling failure.
38
+
39
+ Owner keys are captured before asynchronous work. A stale settings refresh returns without reading its invalidated context; shortcut writes combine the caller and owner signals. Permission operations revalidate ownership, revision, loadout, and targets. Cancellation closes transient UI, not a persisted restriction.
40
+
41
+ ### Settings and terminal compatibility
42
+
43
+ The common screen uses Pi's native `SettingsList` and `getSettingsListTheme()`, an input submenu, and Kit's bounded frame with content-row mapping for mouse routing. The mapped frame implementation preserves the same framing rules as `renderBoundedFrame()`. Unknown fields and invalid-file protection stay in the two owning stores; queues provide in-process ordering, not a cross-process lock.
44
+
45
+ Tests cover immediate hidden-file saves, queued/invalid context blocking only the shortcut, rapid action order, write failures and retry, reload failure, stale continuation, disposal, pending-write drain, input focus, paste, width/height limits, callback theme roles, terminal modes, and non-default keybindings. Host-owned disposal settles the local flow without calling Pi's `done()`, so native dialog-reset iteration and the replacement editor draft stay intact. Configured native actions retain their priority over remapped Escape; only Ctrl+C remains a hard cancel. The 63-case Settings suite includes real Pi reset methods, two dialogs, delayed writes, aliases, modifier order, legacy collisions, terminal modes, invalid bindings, and fallback selection. The explorer returns no cells for zero or unusable widths instead of forcing a one-cell frame; all seven modes have boundary regressions, and normal-width behavior is unchanged. Physical key delivery is not established by these tests.
46
+
47
+ Compatibility exceptions are intentional: legacy commands keep their dedicated managers and settings routes; settings basenames and historical `/scope` notice text remain unchanged; the scope picker retains its bottom-overlay layout. The external editor does not become scope-enforced. The new combined widget uses only the `files` key, keeps the restriction and exit route separate from context summaries, and clears only its owned key.
48
+
49
+ ### Model-visible context
50
+
51
+ No tools or prompt metadata are registered, activated, removed, or rewritten by this merge. Existing scope notice bytes, entry IDs, anchors, quote serialization, and additive publication are retained. Ordinary requests preserve normalized model-visible prefixes; compaction recovery remains deterministic and deduplicated after the original contract disappears. Merged tests exercise quote attachment with active scope and restoration; predecessor contract/runtime suites retain delayed-publication, branch, compaction, and normalized-prefix cases. Package, command-menu, and widget naming changes are not model-prefix transitions.
52
+
53
+ ## Verification evidence
54
+
55
+ Applicable guides: `docs/extension-conventions.md`, `docs/extension-settings.md`, `docs/readme-conventions.md`, `docs/runtime-builder.md`, and the retained package guidance. Semantic review covers all touched areas above rather than treating root gates as a complete behavior audit.
56
+
57
+ Completed verification:
58
+
59
+ - Root `npm install` and `npm ls @narumitw/pi-tui-kit`: passed; the successor resolves to the workspace version satisfying its manifest floor.
60
+ - `npm run check`: passed builds, Biome, package boundaries, and all workspace typechecks.
61
+ - Final full `npm test`: 930 files passed; 20,215 tests passed and 34 skipped, with no unhandled errors. The emitted Settings test fixture was newer than its final source and included all 63 cases.
62
+ - Final focused package/production suites: 39 files passed; 708 tests passed and one existing skip.
63
+ - Native CLI smokes passed explicit generated loading, trusted-project generated auto-discovery, trust denial, RPC, print, JSON, observable scope/Files command rejection, and shutdown. No provider request was sent. The discovery fixture supplies a temporary dependency ancestor to model a real package installation under Jiti's lexical resolution.
64
+ - Bidirectional migration, exact historical notice/anchor bytes, generated Jiti lazy browsing, and production Git-install fixtures passed.
65
+ - Explicit successor build, `npm run package:pack -- files`, and actual temporary packing passed. The tarball contains 64 files, including runtime, source, documentation, changelog, README, and license; tests and build scripts are absent. Packed runtime hashes equal the validated generated output.
66
+ - Clean tarball installation outside the workspace passed dependency resolution, actual CLI smokes, and a lazy browser/startup/shutdown smoke through that installation's own Pi SDK and Kit. Use canonical temporary paths on macOS; the initial `npm --prefix` fixture produced misleading local-file metadata across `/tmp` aliases, so the verified fixture installs from its canonical working directory.
67
+ - Fenced-code-aware heading audits passed for 37 READMEs, with 85 local links/anchors checked. All 79 archived predecessor files retain exact content except the two explicit README warnings. Five scope policy/persistence modules are byte-identical, and eight compiler-normalized quote/discovery functions retain exact bodies.
68
+ - Changesets status passed for the successor only. A read-only successor registry query returned E404; no package was published.
69
+ - Final semantic review covered correctness, security, lifecycle, compatibility, regressions, and scope. Review-found native-work, host-disposal, native-key priority, and project-install-directory issues were fixed with representative regressions or corrected instructions. There are no intentional model-prefix changes or unresolved implementation findings.
70
+
71
+ ### Startup samples
72
+
73
+ Separate-process samples used the existing startup benchmark, cold Jiti cache mode, one warm-up, and three measured runs per configuration on macOS/arm64. Values are medians, not a deterministic gate or a provider-latency claim.
74
+
75
+ | Loadout | Package import time | First command response |
76
+ | --- | --- | --- |
77
+ | Archived predecessor source pair | 266 ms | 396.61 ms |
78
+ | Successor source entrypoint | 199 ms | 329.20 ms |
79
+ | Successor generated entrypoint | 147 ms | 275.59 ms |
80
+
81
+ All samples used isolated agent directories and sent only command-discovery requests. An intermediate full test run had seven process-startup/deadline failures in four unchanged suites; all 53 tests in those suites passed on a focused rerun, and the final full gate passed without concurrent build/smoke commands or timeout changes. Existing unrelated Biome schema/version informational notices remain; checks exit successfully. Physical-terminal and unavailable-platform items below remain external handoff, not passing automated results.
82
+
83
+ ## Retained external handoff
84
+
85
+ ### Physical terminal and unavailable platforms
86
+
87
+ Responsible role: terminal/platform tester. Prerequisites: the built successor and the recorded deterministic/offline loading evidence. Exercise browsing, content search, exact-line review, scope apply/off, external editing, hard cancellation, reload, custom keys, paste/IME, and narrow/fullscreen layouts on the target terminal. Exercise native Windows path behavior on Windows.
88
+
89
+ Expected result: usable input, focus, terminal restoration, and the documented guard boundary. Unverified until performed: physical key delivery, subjective usability, unavailable-platform behavior, and native Windows editor launch. Do not claim those environments were verified. Record any discovered defect and its release impact before making a compatibility claim; deterministic checks do not establish product acceptance.
90
+
91
+ ### First publication
92
+
93
+ Responsible role: package owner. Prerequisites: final implementation evidence, registry-name/ownership/visibility verification, and explicit publication approval. Approve and perform first publication through the repository's approved path; approve npm deprecation notices separately.
94
+
95
+ Expected result: an independently installable successor with accurate migration instructions. Unverified until performed: registry ownership, first-publication visibility, and installation of the released npm artifact. No publication, visibility change, version tag, release workflow dispatch, or npm deprecation is authorized by this merge. Local tarball verification is not release approval.
96
+
97
+ ## Recovery
98
+
99
+ Follow [migration and rollback](migration.md). Stop the run, record resource scopes and target state, remove or disable the currently active implementation, and reload a coherent successor or predecessor resource set without co-loading guards. Retain both settings files and the original session entries. Do not delete state or bypass a guard to recover unrestricted editing; uncertain restoration remains blocked until repaired or explicitly ended.
package/package.json CHANGED
@@ -1,6 +1,60 @@
1
1
  {
2
2
  "name": "@narumitw/pi-files",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Browse and attach file context, and independently limit native Pi edits to selected targets.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "private": false,
8
+ "keywords": [
9
+ "pi-package",
10
+ "pi-extension",
11
+ "pi",
12
+ "file-explorer",
13
+ "git",
14
+ "context",
15
+ "quote",
16
+ "edit-scope",
17
+ "file-editing",
18
+ "allowlist"
19
+ ],
20
+ "files": [
21
+ "src",
22
+ "dist",
23
+ "docs",
24
+ "README.md",
25
+ "CHANGELOG.md",
26
+ "LICENSE"
27
+ ],
28
+ "pi": {
29
+ "extensions": [
30
+ "./dist/index.ts"
31
+ ]
32
+ },
33
+ "scripts": {
34
+ "build": "node scripts/build-runtime.mjs",
35
+ "check": "npm run build && biome check . && npm run typecheck",
36
+ "format": "biome check --write .",
37
+ "typecheck": "tsc --noEmit",
38
+ "prepack": "npm run build"
39
+ },
40
+ "dependencies": {
41
+ "@narumitw/pi-tui-kit": "^0.69.0"
42
+ },
43
+ "peerDependencies": {
44
+ "@earendil-works/pi-agent-core": "*",
45
+ "@earendil-works/pi-coding-agent": "*",
46
+ "@earendil-works/pi-tui": "*"
47
+ },
48
+ "devDependencies": {
49
+ "@earendil-works/pi-agent-core": "1.5.0",
50
+ "@earendil-works/pi-coding-agent": "1.5.0",
51
+ "@earendil-works/pi-tui": "1.5.0",
52
+ "esbuild": "0.28.2",
53
+ "typescript": "7.0.2"
54
+ },
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/narumiruna/pi-extensions",
58
+ "directory": "packages/pi-files"
59
+ }
60
+ }
@@ -0,0 +1,204 @@
1
+ import type { KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Input, Key, matchesKey, type TUI, visibleWidth } from "@earendil-works/pi-tui";
3
+ import { type ContentSearchMatch, searchProjectContents } from "./content-search.js";
4
+ import { contentSearchCardCapacity, renderContentSearchScreen } from "./content-search-ui.js";
5
+ import type { LoadedProjectTextFile } from "./file-context.js";
6
+
7
+ interface ContentSearchSessionOptions {
8
+ tui: TUI;
9
+ theme: Theme;
10
+ keybindings: KeybindingsManager;
11
+ files: readonly string[];
12
+ cwd?: string;
13
+ loadFile: (path: string, signal?: AbortSignal) => Promise<LoadedProjectTextFile>;
14
+ onPreview: (match: ContentSearchMatch) => void;
15
+ onReference: (path: string) => void;
16
+ onSwitchFiles: () => void;
17
+ onCancel: () => void;
18
+ }
19
+
20
+ export class ContentSearchSession {
21
+ private readonly input = new Input();
22
+ private matches: ContentSearchMatch[] = [];
23
+ private selectedIndex = 0;
24
+ private scrollOffset = 0;
25
+ private caseSensitive = false;
26
+ private fuzzy = false;
27
+ private truncated = false;
28
+ private skippedFiles = 0;
29
+ private loading = false;
30
+ private request = 0;
31
+ private controller: AbortController | undefined;
32
+ private error: string | undefined;
33
+ private disposed = false;
34
+
35
+ constructor(private readonly options: ContentSearchSessionOptions) {}
36
+
37
+ set focused(value: boolean) {
38
+ this.input.focused = value;
39
+ }
40
+
41
+ activate(): void {
42
+ this.disposed = false;
43
+ if (this.input.getValue().trim()) this.startSearch();
44
+ }
45
+
46
+ deactivate(): void {
47
+ this.focused = false;
48
+ this.cancelSearch();
49
+ }
50
+
51
+ render(width: number, availableRows: number, opening: boolean, externalError?: string): string[] {
52
+ const capacity = contentSearchCardCapacity(availableRows);
53
+ this.keepSelectionVisible(capacity);
54
+ const queryLabel = this.options.theme.fg("muted", "Search: ");
55
+ const queryWidth = Math.max(1, width - visibleWidth(queryLabel));
56
+ const queryLine = `${queryLabel}${this.input.render(queryWidth)[0] ?? ""}`;
57
+ return renderContentSearchScreen({
58
+ theme: this.options.theme,
59
+ width,
60
+ availableRows,
61
+ queryLine,
62
+ query: this.input.getValue(),
63
+ cwd: this.options.cwd,
64
+ matches: this.matches,
65
+ selectedIndex: this.selectedIndex,
66
+ scrollOffset: this.scrollOffset,
67
+ caseSensitive: this.caseSensitive,
68
+ fuzzy: this.fuzzy,
69
+ loading: this.loading,
70
+ opening,
71
+ truncated: this.truncated,
72
+ skippedFiles: this.skippedFiles,
73
+ error: externalError ?? this.error,
74
+ });
75
+ }
76
+
77
+ handleInput(data: string): void {
78
+ if (this.disposed) return;
79
+ if (matchesKey(data, Key.ctrl("f"))) {
80
+ this.options.onSwitchFiles();
81
+ return;
82
+ }
83
+ if (matchesKey(data, Key.escape)) {
84
+ this.options.onCancel();
85
+ return;
86
+ }
87
+ if (matchesKey(data, Key.alt("c"))) {
88
+ this.caseSensitive = !this.caseSensitive;
89
+ this.startSearch();
90
+ return;
91
+ }
92
+ if (matchesKey(data, Key.alt("f"))) {
93
+ this.fuzzy = !this.fuzzy;
94
+ this.startSearch();
95
+ return;
96
+ }
97
+ if (this.options.keybindings.matches(data, "tui.select.up")) {
98
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
99
+ return;
100
+ }
101
+ if (this.options.keybindings.matches(data, "tui.select.down")) {
102
+ this.selectedIndex = Math.min(Math.max(0, this.matches.length - 1), this.selectedIndex + 1);
103
+ return;
104
+ }
105
+ if (this.options.keybindings.matches(data, "tui.select.pageUp")) {
106
+ this.selectedIndex = Math.max(0, this.selectedIndex - 10);
107
+ return;
108
+ }
109
+ if (this.options.keybindings.matches(data, "tui.select.pageDown")) {
110
+ this.selectedIndex = Math.min(Math.max(0, this.matches.length - 1), this.selectedIndex + 10);
111
+ return;
112
+ }
113
+ if (this.options.keybindings.matches(data, "tui.select.confirm")) {
114
+ const match = this.matches[this.selectedIndex];
115
+ if (match) this.options.onPreview(match);
116
+ return;
117
+ }
118
+ if (this.options.keybindings.matches(data, "tui.input.tab")) {
119
+ const match = this.matches[this.selectedIndex];
120
+ if (match) this.options.onReference(match.path);
121
+ return;
122
+ }
123
+
124
+ const previousQuery = this.input.getValue();
125
+ this.input.handleInput(data);
126
+ if (this.input.getValue() !== previousQuery) this.startSearch();
127
+ }
128
+
129
+ invalidate(): void {
130
+ this.input.invalidate();
131
+ }
132
+
133
+ dispose(): void {
134
+ if (this.disposed) return;
135
+ this.disposed = true;
136
+ this.cancelSearch();
137
+ }
138
+
139
+ private startSearch(): void {
140
+ this.cancelSearch();
141
+ const query = this.input.getValue();
142
+ this.matches = [];
143
+ this.selectedIndex = 0;
144
+ this.scrollOffset = 0;
145
+ this.truncated = false;
146
+ this.skippedFiles = 0;
147
+ this.error = undefined;
148
+ if (!query.trim()) return;
149
+
150
+ const request = this.request;
151
+ const controller = new AbortController();
152
+ this.controller = controller;
153
+ this.loading = true;
154
+ void searchProjectContents(this.options.files, this.options.loadFile, query, {
155
+ caseSensitive: this.caseSensitive,
156
+ fuzzy: this.fuzzy,
157
+ signal: controller.signal,
158
+ })
159
+ .then((result) => {
160
+ if (!this.isCurrent(request, controller)) return;
161
+ this.matches = result.matches;
162
+ this.truncated = result.truncated;
163
+ this.skippedFiles = result.skippedFiles;
164
+ })
165
+ .catch((error: unknown) => {
166
+ if (this.isCurrent(request, controller) && !isAbortError(error)) {
167
+ this.error = formatError(error);
168
+ }
169
+ })
170
+ .finally(() => {
171
+ if (request === this.request) {
172
+ this.loading = false;
173
+ this.controller = undefined;
174
+ }
175
+ if (!this.disposed) this.options.tui.requestRender();
176
+ });
177
+ }
178
+
179
+ private cancelSearch(): void {
180
+ this.request += 1;
181
+ this.controller?.abort();
182
+ this.controller = undefined;
183
+ this.loading = false;
184
+ }
185
+
186
+ private isCurrent(request: number, controller: AbortController): boolean {
187
+ return !this.disposed && request === this.request && this.controller === controller && !controller.signal.aborted;
188
+ }
189
+
190
+ private keepSelectionVisible(capacity: number): void {
191
+ if (this.selectedIndex < this.scrollOffset) this.scrollOffset = this.selectedIndex;
192
+ if (this.selectedIndex >= this.scrollOffset + capacity) {
193
+ this.scrollOffset = this.selectedIndex - capacity + 1;
194
+ }
195
+ }
196
+ }
197
+
198
+ function isAbortError(error: unknown): boolean {
199
+ return error instanceof Error && error.name === "AbortError";
200
+ }
201
+
202
+ function formatError(error: unknown): string {
203
+ return error instanceof Error ? error.message : String(error);
204
+ }
@@ -0,0 +1,159 @@
1
+ import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
3
+ import type { ContentMatchRange, ContentSearchMatch } from "./content-search.js";
4
+
5
+ export const CONTENT_SEARCH_CARD_ROWS = 3;
6
+
7
+ interface ContentSearchScreenOptions {
8
+ theme: Theme;
9
+ width: number;
10
+ availableRows: number;
11
+ queryLine: string;
12
+ query: string;
13
+ cwd?: string;
14
+ matches: readonly ContentSearchMatch[];
15
+ selectedIndex: number;
16
+ scrollOffset: number;
17
+ caseSensitive: boolean;
18
+ fuzzy: boolean;
19
+ loading: boolean;
20
+ opening: boolean;
21
+ truncated: boolean;
22
+ skippedFiles: number;
23
+ error?: string;
24
+ }
25
+
26
+ export function contentSearchCardCapacity(availableRows: number): number {
27
+ return Math.max(1, Math.floor((availableRows - 4) / CONTENT_SEARCH_CARD_ROWS));
28
+ }
29
+
30
+ export function renderContentSearchScreen(options: ContentSearchScreenOptions): string[] {
31
+ const { theme, width, availableRows } = options;
32
+ const capacity = contentSearchCardCapacity(availableRows);
33
+ const cwdLabel = options.cwd ? ` · cwd: ${escapeTerminalControls(options.cwd)}` : "";
34
+ const title = theme.fg("accent", theme.bold(`File Context · Content Search${cwdLabel}`));
35
+ const modes = theme.fg(
36
+ "muted",
37
+ `Case: ${options.caseSensitive ? "on" : "off"} · Fuzzy: ${options.fuzzy ? "on" : "off"} · Alt+C case · Alt+F fuzzy`,
38
+ );
39
+ const body: string[] = [];
40
+
41
+ if (options.error) {
42
+ body.push(theme.fg("error", ` Search failed: ${escapeTerminalControls(options.error)}`));
43
+ } else if (options.loading) {
44
+ body.push(theme.fg("warning", " Searching…"));
45
+ } else if (!options.query.trim()) {
46
+ body.push(theme.fg("muted", " Type text to search project contents"));
47
+ } else if (options.matches.length === 0) {
48
+ body.push(
49
+ theme.fg(
50
+ "muted",
51
+ ` No matches for "${escapeTerminalControls(options.query)}"${options.fuzzy ? "" : " · Alt+F enables fuzzy matching"}`,
52
+ ),
53
+ );
54
+ } else {
55
+ const visible = options.matches.slice(options.scrollOffset, options.scrollOffset + capacity);
56
+ for (let visibleIndex = 0; visibleIndex < visible.length; visibleIndex += 1) {
57
+ const index = options.scrollOffset + visibleIndex;
58
+ const match = visible[visibleIndex];
59
+ if (match) {
60
+ body.push(...renderContentSearchCard(match, index === options.selectedIndex, width, theme));
61
+ }
62
+ }
63
+ }
64
+
65
+ const count = `${options.matches.length}${options.truncated ? "+" : ""} matches`;
66
+ const skipped = options.skippedFiles > 0 ? ` · ${options.skippedFiles} skipped` : "";
67
+ const action = options.opening
68
+ ? "Opening…"
69
+ : `${count}${skipped} · ↑↓ navigate · Enter preview · Tab reference · Ctrl+F files · Esc cancel`;
70
+ return fitRows(
71
+ [
72
+ truncateToWidth(title, width, ""),
73
+ truncateToWidth(options.queryLine, width, ""),
74
+ truncateToWidth(modes, width, ""),
75
+ ...body.map((line) => truncateToWidth(line, width, "")),
76
+ truncateToWidth(theme.fg(options.opening ? "warning" : "muted", action), width, ""),
77
+ ],
78
+ availableRows,
79
+ );
80
+ }
81
+
82
+ export function highlightContentRanges(line: string, ranges: readonly ContentMatchRange[], theme: Theme): string {
83
+ let result = "";
84
+ let index = 0;
85
+ for (const range of ranges) {
86
+ const start = Math.max(index, Math.min(line.length, range.start));
87
+ const end = Math.max(start, Math.min(line.length, range.end));
88
+ result += escapeTerminalControls(line.slice(index, start));
89
+ result += theme.fg("warning", theme.bold(escapeTerminalControls(line.slice(start, end))));
90
+ index = end;
91
+ }
92
+ return result + escapeTerminalControls(line.slice(index));
93
+ }
94
+
95
+ function renderContentSearchCard(match: ContentSearchMatch, selected: boolean, width: number, theme: Theme): string[] {
96
+ if (width < 8) {
97
+ return [truncateToWidth(`${selected ? ">" : " "}${escapeTerminalControls(match.path)}`, width, "")];
98
+ }
99
+ const prefix = selected ? theme.fg("accent", "> ") : " ";
100
+ const cardWidth = Math.max(4, width - visibleWidth(prefix));
101
+ const innerWidth = Math.max(1, cardWidth - 2);
102
+ const borderColor = selected ? "borderAccent" : "borderMuted";
103
+ const border = (text: string) => theme.fg(borderColor, text);
104
+ const rawTitle = ` ${escapeTerminalControls(match.path)} · L${match.lineNumber} `;
105
+ const title = truncateToWidth(rawTitle, innerWidth, "");
106
+ const titleFill = "─".repeat(Math.max(0, innerWidth - visibleWidth(title)));
107
+ const context = clipMatchContext(match, Math.max(1, innerWidth - 1));
108
+ const highlighted = highlightContentRanges(context.line, context.ranges, theme);
109
+ const content = truncateToWidth(` ${highlighted}`, innerWidth, "");
110
+ const contentFill = " ".repeat(Math.max(0, innerWidth - visibleWidth(content)));
111
+ const contentLine = `${border("│")}${content}${contentFill}${border("│")}`;
112
+ return [
113
+ `${prefix}${border("╭")}${title}${border(`${titleFill}╮`)}`,
114
+ `${" ".repeat(visibleWidth(prefix))}${selected ? theme.bg("selectedBg", contentLine) : contentLine}`,
115
+ `${" ".repeat(visibleWidth(prefix))}${border(`╰${"─".repeat(innerWidth)}╯`)}`,
116
+ ];
117
+ }
118
+
119
+ function clipMatchContext(
120
+ match: ContentSearchMatch,
121
+ maxCharacters: number,
122
+ ): { line: string; ranges: ContentMatchRange[] } {
123
+ const firstStart = match.ranges[0]?.start ?? 0;
124
+ const lastEnd = match.ranges.at(-1)?.end ?? firstStart;
125
+ const matchSpan = Math.max(1, lastEnd - firstStart);
126
+ const before = Math.max(0, Math.floor((maxCharacters - Math.min(matchSpan, maxCharacters)) / 3));
127
+ const rawStart = Math.max(0, firstStart - before);
128
+ const prefix = rawStart > 0 ? "…" : "";
129
+ const availableRawCharacters = Math.max(1, maxCharacters - prefix.length - 1);
130
+ let rawEnd = Math.min(match.line.length, rawStart + availableRawCharacters);
131
+ if (rawEnd < lastEnd) rawEnd = Math.min(match.line.length, lastEnd);
132
+ const suffix = rawEnd < match.line.length ? "…" : "";
133
+ const line = `${prefix}${match.line.slice(rawStart, rawEnd)}${suffix}`;
134
+ const ranges = match.ranges.flatMap((range): ContentMatchRange[] => {
135
+ const start = Math.max(rawStart, range.start);
136
+ const end = Math.min(rawEnd, range.end);
137
+ return end > start ? [{ start: prefix.length + start - rawStart, end: prefix.length + end - rawStart }] : [];
138
+ });
139
+ return { line, ranges };
140
+ }
141
+
142
+ function fitRows(lines: string[], height: number): string[] {
143
+ if (lines.length <= height) return lines;
144
+ if (height <= 1) return lines.slice(0, 1);
145
+ return [...lines.slice(0, height - 1), lines.at(-1) ?? ""];
146
+ }
147
+
148
+ function escapeTerminalControls(text: string): string {
149
+ return [...text]
150
+ .map((character) => {
151
+ if (character === "\t") return " ";
152
+ const code = character.charCodeAt(0);
153
+ if (code <= 31 || (code >= 127 && code <= 159)) {
154
+ return `\\x${code.toString(16).padStart(2, "0")}`;
155
+ }
156
+ return character;
157
+ })
158
+ .join("");
159
+ }