Repository navigation
feat: project services, commands lich starts and keeps running - #930
Merged
Merged
Conversation
A project can save services: a container, a tunnel, a dev server. lich runs each one in the project's directory on a PTY of its own, without the user's rc files and with no shell behind it, so when the command exits the service has stopped and its last output stays readable until it starts again. Ctrl+C at its console and Stop both read as a stop, not a failure. Backend: a project_services table (migration 11), the services.* RPCs and the services and service-reveal events, internal/services for the state of every run, terminal.StartService for the PTY, Start with lich at boot and a graceful stop of every service on quit. Service PTYs are not sessions: they stay out of LiveCount and the turn-state catch-up. The tray lists the running services with Show output, Restart and Stop. Window: a Services row in the sidebar footer above the profile, closed to a count and open upward to a row per service; the same list from a Services button on the collapsed rail, which also shows the profile now; a console in place of the session terminal; Settings > Services per project; a Start the project's services action in the palette; the close dialog counts services, so closing a window with only services running asks instead of quitting. TestOpenDropsTheTerminalThemeSetting drops project_services with the other tables later migrations add, because the version-0 database it rebuilds never had it. TestOnlyProjectScopedSectionsStayLitInAProject lists Services, a new project-scoped section.
…oter A service now lives as long as its project's tab. Closing the tab stops the project's services (killed past a 10 second grace), reopening the project starts the ones saved to start with lich, and lich starting brings up only those whose project has a tab open. Home has no tab to close, so its services run as long as lich does; Settings > Services edits them with Global selected. The tray only ever lists services of open projects, so Show output no longer reopens a tab. In the window, the running dot of the Services row, the rail badge and the console bar beats: a ring grows out of it for 0.8 seconds at the spinner's rate, then rests with no frames until the next beat 2.4 seconds on. A continuous pulse at a tenth of that rate held the window at 6% of a core. The rows keep a plain dot. Start all is an icon with its words in a tooltip, and the service list no longer reserves a scrollbar gutter, so its rows end where the cards above them do.
#932 gave macOS a menu-bar item on AppKit through purego. Its menu now carries what the Linux and Windows tray does: the running services count, a slot per running service with Show output, Restart and Stop, and Stop all services. A slot's submenu entries are told apart by their tag, and the logic behind them is the one the other trays share. The ceiling that said macOS has no tray names the eight-slot limit instead, and that these entries never ran on a Mac.
omartelo
force-pushed
the
feat/project-services
branch
from
October 24, 2026 01:37
3b6d337 to
beac4b9
Compare
TestADoneWithOneErrandOpenStallsItWithoutANotice failed on CI with "writes = 2, want no notice". stampDelivery marks a ticket delivered before the paste is written, and the Enter follows a beat later, so the test, which counted the target's writes as soon as it saw the ticket delivered, could count the paste alone and then read the Enter as a notice. Holding the Enter back 30ms in the fake terminal made it fail 5 runs out of 5; waiting for the ticket to be submitted, which is after both writes, passes all 5 with the same delay. The two-errand test beside it counted the same way and waits the same way now. The test asserted something the contract never promised: that a delivered ticket's writes are all in.
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.
Summary
Replaces the per-card entrypoint removed in #849 with services that belong to a project. A user who starts the same container, tunnel or dev server every day saves it once in Settings › Services and lich runs it.
$SHELL -c(PowerShell-NoProfileon Windows) in the project's directory, without rc files, on a PTY of its own. When the command exits the service has stopped: nothing is respawned, the last output stays readable, and Start runs it again. Ctrl+C at the console and Stop are a stop; an exit of its own with a code other than 0 is a failure.lazydockerkept open whatever the project.up/total; open, it grows upward with one 26px row per service (dot, name, uptime or how it ended; Start, Stop and Restart on hover; right-click for Show output, Edit and Delete) and New service. A project with no service shows the row dimmed with Add.Design and journey: https://claude.ai/artifact/BE6bLttP9ezYVJLdvp6tGA
Contract
api/services.go:services.List/Save/Delete/Start/Stop/ForceStop/Restart/StartAll/StopAll/Output, theservicesevent (the whole list, emitted once a change is true) andservice-reveal.TrayLabelsgains the service labels.contract.goldenonly adds.project_services(cascade-deleted with its project, name unique per project).Tests changed, and why
TestOpenDropsTheTerminalThemeSettingdropsproject_serviceswith the other objects later migrations add: the version-0 database it rebuilds never had the table.TestOnlyProjectScopedSectionsStayLitInAProjectlists Services, a new project-scoped section (the contract changed).i18n_shell_test.gopasses the new services count tocloseRunning; its assertions are unchanged.Ceilings
New
docs/ceilings.mdsection: a detaching command shows stopped, no rc except under fish, a service lives as long as its project's tab (an update's restart starts only the Start with lich services), output kept in memory only, at most eight services named in the tray (the macOS entries never ran on a Mac), Stop as Ctrl+C through ConPTY on Windows.Test plan
internal/store,internal/terminal(real PTYs),internal/services,internal/tray,internal/routes,apiandnativeunit tests; service suites run 20 times each without a failure.go test ./...for the backend andnative/, and the linux/darwin/windows cross-compile loop.