Experiment: Replace CLI UX with much broader TUI app/IDE#5689
Draft
StachuDotNet wants to merge 151 commits into
Draft
Experiment: Replace CLI UX with much broader TUI app/IDE#5689StachuDotNet wants to merge 151 commits into
StachuDotNet wants to merge 151 commits into
Conversation
…ions, narrow-collapse)
…orkbench'/'wb' command
…e pane + Tab focus
… behind DARK_CLASSIC=1 — P1 complete
…; dev-drive final-frame + window-size opts
…; digit-switch reloads body
…uns/Mesh, then polish
…ViewName — N items)
…peek, finalize 2am
…ve->Conflicts, Mesh->Devices, my Matter->SCM) 1-9 only reach the first nine of 14 views; ` now opens a jumpable picker of all views (↑↓/j/k, ⏎ or a digit to jump, esc). Footer advertises it. Also fixes the naming confusion Stachu flagged: the Tree IS Dark Matter, so Tree->Matter; the SCM overview I'd mislabeled 'Matter' is now 'SCM'; Resolve->Conflicts, Mesh->Devices. (gotcha: it's Stdlib.String.padStart, not padLeft — runtime-only failure.)
…he tree at once Edit view: n/enter opens a full editor prefilled with 'module <you>.Drafts' + a declaration. saveEditing now honors a leading 'module Owner.Mod' line — parses it off as the target and applies the declaration there — so you write the whole thing and ^s applies it without any navigation. Verified: Edit -> n -> ^s saves example into Stachu.Drafts.
…e stats) Inspect as a top-level tab had no selection context and rendered nothing useful; it now shows an at-a-glance overview of Dark Matter — the package-stats view (top-level modules, fn/type/value counts, per-module table) captured into the pane. (It's still Matter's live right-hand detail pane too.)
… / a acknowledge Top gap from the command audit: Home and the tab badge scream 'N to resolve' but the view was read-only. Now the selected divergence resolves in place — o keeps your local hash, t keeps the incoming, a acknowledges — via Sync.Conflicts.keep/acknowledge (same as the classic conflicts command). Footer advertises the keys; empty case is safe (verified no crash).
…eps-dependents + apps-run next
The audit noted Tree d only did dependencies while the classic deps command does both. depsText now shows two sections — Dependencies (uses) via depsGetDependencies + Dependents (used by) via depsGetDependents [(location, kind)], names resolved. Verified on parsePTExpr (3 uses / 0 used-by).
Cursor onto a `render` fn in the Matter tree and its output renders right there in the Inspect pane (capture-eval of `Module.render ()`), instead of source. renderPreview skips re-highlighting content that's already ANSI-colored, so the rendered frame shows clean. Verified on Apps.Views.AiChats.render -> the live thread board in Inspect. This is the tree-native version of the Apps&Views peek.
…c-prompt fallback The workbench is the experience now. Escape (and q) exit the CLI straight to the shell instead of backing out to Home or dropping to the classic prompt. Removed the leave-for-classic-prompt confirm; the classic prompt stays reachable only via DARK_CLASSIC=1 at launch. Old commands live on the `:` bar, not behind an exit. Exit leaves the alt-screen cleanly (verified: rc=0, shell restored).
…keys The ? keymap said 'esc back / q quit' (pre-'Escape just leaves') and listed old Changes/History groups. Now: Global shows 'esc/q quit' + the backtick view picker, and there are accurate Conflicts (o/t/a) and SCM (commit/discard/branch/switch/merge/ rebase) rows. Also dropped the long dead helpText val (superseded by helpGroups).
…, add REPL, reframe Devices/AI/Apps/Traces Collapses the 14 tabs to 10 (Home Matter Inspect REPL SCM Devices AI Apps Traces Docs): - SCM absorbs Changes/History/Conflicts as tab-switched sections (conflict resolution lives here) - Inspect rebuilt as the item page (source + used-by/uses references), not fake stats - REPL: persistent eval log + a (faked) live-values sidebar - Devices reframed around instances + sync (no tailscale branding) - AI/Apps/Traces get honest reframed shells; Apps reads the real registry - Values folded into Matter as a lens (V); Edit demoted to the n/e modal it already was - Broad UX: view-index constants (single source of truth), context line (the pwd), partial shared cursor across views, in-view filter (f), consistent keymap Compiles clean via reload-packages. Interactive run in this clone is blocked by a packages-source config quirk (run-cli reads main-branch Cli packages), tracked separately.
… isolation fix With 2+ dark containers up, ./scripts/* picks by `docker ps --last`, not the current clone, so it can silently run in another clone's container (edits look ignored, wrong branch's behavior). Documents how to check, and the dark-multi-style fix (per-clone build volume + remapped ports) for coexisting backend-divergent clones.
Expand the multi-clone section into the whole dance learned the hard way: pinning work to a clone's container via explicit docker exec; the shared dark_build + fixed-ports trap and the dark-multi-style isolation fix; and the fresh-container bootstrap order (build via _build-server not bare compile; fresh-from-schema DB not stale seed, whose Release stamp skips the origin_ts migration; empty package-ref-hashes.txt before reload).
The Values lens (V in Matter) lists items by full path, but detailLines looked them up as leaf-in-current-location, so the preview/reader showed '(not found)'. Route detailLines through thingDetail (full-path lookup) when the lens is active. Found by driving the running build.
startNew prefilled 'owner. + join(modPath)', but modPath (the current location) already starts with the owner, so authoring from inside /Stachu offered 'Stachu.Stachu.'. Use modPath as-is when non-empty; fall back to the account owner only at the root. Found by driving the running build.
- Devices: a real sectioned device/sync manager — this instance, example peers (online/offline dots), per-branch sync status — instead of a wall of prose - Traces: demo trace set when none are recorded, so the surface isn't just empty; richer detail (replay / make-test / walk hints) - Home: fixed stale nav hints (were 'Tree/Changes/Resolve' + 'press 7'); now points at the real views - breadcrumb: non-list views (Home/REPL/SCM/Devices) show their name, not '— 0 items' Verified rendering live.
AI was just the demo agents board; add a Prompts subsection — a preview library of reusable prompts (review-diff, gen-tests, name-this, ...) with a detail pane — and a Tab switcher between Agents and Prompts, mirroring SCM's sections. Threads aiSection through itemsForView/preview like scmSection. Fills out the last thin tab.
- Inspect item page gains a metadata line: kind · real content hash · stability (hash is genuine content-addressing; deprecation marker is placeholder) - Apps 'i' now actually installs/uninstalls the selected app (updates the installed set and the ●/○ marker live), instead of pointing at the : bar Both verified live.
The surprise was that Enter did nothing in Apps while install hid behind 'i'. Make ⏎ the primary row action (install/uninstall), extract toggleInstall so ⏎ and i share it, footer leads with '⏎ install'. Step toward: Enter = the row's main action, everywhere.
… lens, Devices opt-in - ⏎ is now the row's primary action across views: Apps install, AI Prompts 'use' (fills the ask bar), AI Agents / Traces open the detail. (was: dead Enter, verbs hidden behind letters) - values lens moves off V to L, so it stops clashing with v = new value - Devices is opt-in: hidden from the tab bar / number keys / cycle / picker unless config's devices.enabled is true. Config-driven (cli-config.json via the existing config command / the : bar), not an env var. Tab bar + nav now operate on the visible-view set, so hiding Devices leaves no gap (key 6 → AI, not a hole). Verified live: default 9 tabs, config flips to 10.
…ock data) - context line shows the instance's name (config instance.name, else hostname) instead of 'local', and drops the unclear 'patch: Quick edits' segment - each instance registers a UUID (instance.id) in cli-config.json on first run; name is config-overridable (config set instance.name major) - Devices 'this instance' row uses the same name - every faked surface now says '(mock data)' explicitly, not 'preview' (Devices peers + branch sync, AI agents + prompts, traces detail)
…st 1-9 Number keys don't scale with a variable tab count (and Devices being opt-in already means key N != view N). Add ctrl+left/right to move between the visible tabs (verified it decodes reliably through Console.ReadKey, unlike alt+arrow). Keeps [ ]/cycle, the ` picker, and 1-9 as alternates; help overlay now leads with ctrl+←/→.
Adds Branches as the 4th SCM section (tab-cycled): lists branches with the current one marked, detail pane per branch. ⏎ switches, b new, r rename, x delete — real CRUD over SCM.Branch.*. Verified live (create/switch/delete).
…s/Calendar Refactors view dispatch so the core stops knowing about the extensible surface. Views are now a composed list of `ViewDef` values (id, name, gate, loadItems, render, detail, hints) — the same function-fields-in-a-record shape as `Cli.Registry.CommandHandler`. The core dispatchers (visibleViews / itemsForView / previewLines / renderViewBody / hintsForView) delegate generically to that registry for anything past the built-ins, so adding a view is one `extensionViews` entry plus a few small fns, not edits scattered across five functions. --- Why: adding Errors/Crons/Calendar (and the login-gated per-account views to come) was forcing the main source to name each new view in visibleViews, the item loader, the preview, the body render, the hints, the tab names, and the gating list. That's the opposite of composable. The registry makes the extensible views additive and keeps account-name gating out of source entirely. Shape: - `type ViewDef` holds behavior as function-typed fields. Built-ins (indices 0-9) still live inline in the core because it owns their custom key handling; extension views (10+) are positions in `extensionViews ()`, never referenced by name. - `viewSpecs ()` unifies base + extensions into (index, label, gateKey); visibleViews, the picker, the tab bar, and the launch-time gate all derive from it. - Gating is per-ViewDef `gateKey` resolved by `gateEnabled` (config value = `true`/account-list), so per-user visibility is data the user sets, not `isStachu` in the code. - `Frame.renderTabBar` now takes resolved (index, label) pairs, so the frame no longer owns a name table for extension views. New views (all `(mock data)` labeled): Errors (real conflicts + mock 500s/test/deprecation/perf), Crons (schedule/last/next rows + detail), Calendar (month grid + agenda, gated). Gotcha worth noting: inline lambdas in the registry tuples left the field types as `_` and blocked record coercion — every ViewDef field has to be a named function reference (which is also why CommandHandler is written that way). Verified live: registry resolves at runtime, function-field invocation works, and the tab bar composes/gates correctly. Claude-Session: https://claude.ai/code/session_015BbLwctE41vpgJksNa9X4C
….enabled The first login-tier view (#4.B): a Builtins tab listing all 704 F# builtins with their signature + purity + description in the preview. Real data via `Builtin.getAllBuiltinFns ()`, reusing the `builtins` command's type/purity formatting. It's just another `ViewDef` in the registry, gated on `internal.enabled` — so `config set internal.enabled stachu,feriel` turns the whole Darklang-internal tier on for those two accounts and no one else. No account names in source; the tier is config data. This is the payoff of the registry refactor: a login-gated dev-tools view is one entry, nothing in the core changes. Verified live: the Builtins tab appears in the bar only when the internal gate names the current account, and the list loads all 704 sorted. Also updates the phase-plan notes: 3.A-3.C and 4.A done, 4.B done, 4.C in progress via the gate. Claude-Session: https://claude.ai/code/session_015BbLwctE41vpgJksNa9X4C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Very WIP, not sure where this will go.
Instead of dropping you at a bare prompt where you type a command and read the result, this evolves the default
darkCLI experience into a framed, keyboard-driven IDE over Dark Matter — the synced package space youbrowse, read, author, version, and sync.
darkwith no args opens the workbench; the classic prompt is stillthere for anyone who wants it (
DARK_CLASSIC=1at launch), and every arg'd command (dark status,dark eval, …) is untouched. Branchcli-ux-redux.The old UX was super bare-bones — a prompt, a command, a wall of printed output, and you rebuild the picture in
your head across
ls/view/status/commits/deps. This expands that into separate views andpanes over the same Matter: where you are, what's changed, what it depends on, what it looks like — on one
screen, and you stay in it through the whole author → edit → review → commit → sync loop. I'm less attached to
any single view than to the shape: a persistent home vs. a prompt.
The choices most worth reacting to: commit to it, don't hedge — no args opens the workbench, no chooser,
and the workbench is the experience:
Esc(orq) just leaves, straight to the shell — there's noclassic-prompt fallback to back out into. The old world doesn't live behind an exit, it lives in the frame:
:is an inline command bar that runs any classic command (status,ls,agent ask, …) and shows itsoutput in-frame, so you never leave to reach it. And the noun under all of it is Dark Matter, which syncs —
the Matter view (below) is the home for its state.
The frame
Every view shares one frame: a tab bar (14 views, active one marked
◆), a breadcrumb with the currentpath on the left and branch + sync status on the right, and a footer that only advertises the keys that
actually do something in the current view. All fourteen, real, from a running
dark:Navigation is uniform:
1-9jump to a view,[/]cycle,`opens a picker for all fourteen (thenumber keys only reach the first nine),
↑↓orj/kmove,tabtoggles the focused pane,/searches(Enter jumps to a hit),
?is a keymap overlay,:is the command bar. The footer re-labels itself per view,so the keys you see are the keys that work.
Views
A framed home over the same Matter, one angle per view. Most are a list + live preview split: the list on
the left, and whatever the selection is — source, ops, a diff, a rendered view — on the right.
Home — a dashboard: where you are, what's changed, who you are.
Matter — browse the Matter tree; the selection's source is syntax-highlighted on the right. Icons carry the
kind:
🗂module,#type,•value,⚡fn.Inspect — with nothing drilled in, a live Package Statistics overview: totals and a per-module bar
chart, sortable and filterable.
Edit — author anything and apply it at once, no navigating first. Start a draft and you get a full editor
prefilled with a
module <you>.Draftsheader; write the module + declaration,^sparses the whole thing andapplies it to the tree. Change the
moduleline to save anywhere.Changes / History — your WIP, and the op log. History is a list of commits with the selected commit's detail
(and its ops, on Enter) on the right.
┌─ History · 1 ────────────────────────┐┌────────────────────────────┐ │> • 9fa317f3 Init: packages loaded ││ 9fa317f3… │ │ from disk (10908 ops) ││ Init: packages loaded │ │ ││ 10908 ops · ⏎ to view them │ └───────────────────────────────────────┘└──────────────────────────────┘Conflicts — incoming conflicts, resolvable right here: keep ours, keep theirs, or acknowledge. In sync, it
says so.
┌─ Conflicts ─────────────────────────────────────────────┐ │ ✓ nothing to resolve — in sync │ └──────────────────────────────────────────────────────────┘ ↑↓ move · o keep ours · t keep theirs · a acknowledgeAI — a thread board: each agent thread with status/branch, and the selected thread's detail (cost, tokens,
what it's editing) on the right.
aasks the agent a new prompt. (Board is demo data for now.)Apps & Views — the app registry + registered views; move onto a view and its render is captured live into
the preview pane, so you quick-peek a pretty view just by tabbing.
Values — every package value (447 of them), browsable, with the value's definition on the right.
Docs — the doc set (
for-ai,syntax,types, …), each rendered as markdown in the preview. Some arelive (the Stdlib overview computes its counts).
SCM — one SCM-centric home over Dark Matter: a status header (branch → parent, WIP, commits, conflicts,
peers — the always-true summary) over the change set, with every version-control action in the footer.
Devices / Runs — the tailnet mesh, and recorded traces. Both report their real state honestly rather than
faking data:
Views live in the tree
The nicer version of "peek at a view" isn't a separate registry — it's a renderable value lighting up where it
lives in the Matter tree. Cursor onto a
renderfn anywhere in the tree and its output renders live in thepreview, instead of source. Here,
Apps.Views.AiChats.renderselected in the tree, its board rendered live onthe right:
Overlays
Three modal overlays, reachable from any view.
?— a keymap, grouped and self-labeling per the keys that actually work:`— a jumpable picker for all fourteen views (the number keys reach only the first nine):┌─ Go to view ────────┐ │ 1 Home │ │ > 2 Matter ◆ │ │ 3 Inspect │ │ 4 Edit │ │ … │ │ 14 SCM │ └─────────────────────┘:— the command bar. Runs any classic command and shows its output in-frame, so the whole old CLI isreachable without leaving the workbench:
The loop, end to end: navigate, read source or the op log, author or edit a fn/type/value (
n/t/v+^s,from anywhere — no depth trap), rename (
r, a real one-op rename), review the diff, commit, branch, merge,rebase, resolve conflicts — without leaving the screen.
Reusable UI (generic — not workbench-specific)
Each is a small component with a typed input and a render. Worth a look on their own; any Dark TUI can mount
them.
UI.Box— a focus-aware bordered/titled pane.draw region title focused -> innerRegion.UI.SplitPane— two bordered panes;render region orientation firstPct focus titleA renderA titleB renderB. ComposesBox; the focused pane gets the bright border.UI.ListView— a scrollable/selectable list body:render region (displayLines: List<String>) (selected: Int). Owns the bottom-anchored viewport + the overflow scrollbar; the caller styles its own rows.UI.Editor— a pure multiline text buffer:Buf { lines: List<String>; row: Int; col: Int }withinsertText/newline/backspace/ motion. Eval-testable (no I/O).UI.Layout— the region model (Region,printAt,vstack/hstack,distributeRows/Cols) plustruncateVisible (s: String) (maxVisible: Int) : String— an ANSI-aware truncate that cuts a highlighted lineto a visible width without slicing through a color code. This is what lets highlighted source and captured
command output fit any pane cleanly.
One backend addition (the only non-
.darkbit — worth review):NonBlockingConsolegains a capture buffer +stdoutCaptureStart/Stopbuiltins, so the CLI can run a command (or a view's render) and collect its outputas a string instead of writing to the frame. That's what powers the
:command bar, the AI ask surface, theApps & Views previews, and the tree-native render peek.
Honestly not done, with reasons
switchable sections, and a real +/- diff pane (the diff engine already exists in
scm/review, just needsborrowing), is the fast-follow. See
notes/scm-matter-view-design.md.aasks (agent ask, reply in-frame; needs a provider key or localOllama) and the board is demo data; conversation history / multiple sessions is the real chat UX, still to
come.
render-named fn. The realversion is a by-type signal on the value ("this is renderable"), so any view lights up in the tree without a
naming convention.
SetNametoday, but a Location can only point at packagecontent.
notes/location-reference-design.mdsketchesLocationReference(content-by-hash / DB / alias /SqlDB / file) and the sync boundary — a name always syncs, its target may be Matter or a local handle. That
informs the Matter view and the tree.
layout.dark→resizeable.dark— considered and skipped:Layoutis the accurate name for the regionmodel; the rename would be narrower and risky. Flagging rather than doing it silently.
Devices/Runs show "unavailable/empty" honestly in this env (no tailnet, no traces). Rebase's success path isn't
exercised end-to-end (merge's is).