yydick / vicecode
VSCode-style multi-panel terminal workspace (TUI) built with php-tui and Swoole.
Requires
- php: >=8.3
- ext-swoole: *
- league/commonmark: ^2.10
- php-tui/php-tui: ^0.2
- scrivo/highlight.php: ^9.18
Requires (Dev)
None
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-14 02:43:31 UTC
README
A VSCode-style multi-panel terminal workspace (TUI). Built on PHP 8.3 + php-tui/php-tui 0.2.1 + Swoole 6.2.1, it provides a coordinated six-panel environment in your terminal: Explorer / Editor / Terminal / GIT / Search / AI Chat.
The sidebar holds four tabs: Explorer / GIT / Search / ๐งฉ Plugins. The top menu bar opens with F10 (File / View / Terminal / Help).
โโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโ
โ Sidebar โ Editor โ Terminal โ
โ Explorer โ (highlight/multi-tab/...) โ (cmd/inter) โ
โ GIT โ โ โ
โ Search โ โ โ
โโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโค
โ AI Chat Stream โ AI Input โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโค
โ StatusBar โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
The sidebar holds four tabs: Explorer / GIT / Search / ๐งฉ Plugins; the top menu bar opens with F10 (File / View / Terminal / Help).
Preview
Text mockups below (identical layout in a real terminal). The Terminal panel is a real PTY by default (bash, reading your
~/.bashrc) โ aliases, functions and the prompt are all there, and full-screen programs likevim/top/sshjust work. Once focused, start typing to take over the keyboard;Esc/F2leaves capture;exit/Ctrl+Dends the shell and drops back to "Command Runner".
Interactive terminal (Terminal panel default)
โโ Interactive terminal (Terminal panel default) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ Sidebar โ Editor โ Terminal โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ > Project โ <?php โ ~/work/tui$ ls src โ
โ src โ final class App { โ app.php panel/ ... โ
โ tests โ public function run() { โ ~/work/tui$ grep ... โ
โ vendor โ // edit code โ ... 3 hits โ
โ โ } โ ~/work/tui$ โ โ
โ GIT โ } โ โ
โ * main โ โ โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AI Chat โ AI Input โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโค
โ (model output streams in) โ type message, Enter to โ
โ (here) โ send โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Focus:Edit Branch:main Lang:en Theme:midnight Ctrl+Q quit โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Full-screen programs in the interactive terminal (vim / top โ default mode)
โโ Interactive terminal: vim / top (capturing) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ Sidebar โ Editor โ Terminal โ
โโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโค
โ > Project โ <?php โ $ vim README.md โ
โ src โ final class App { โ ~ VIM โ
โ tests โ // editor still usable โ ~ reading/editingโฆ โ
โ vendor โ } โ ~ โ
โ โ โ :wq โ
โ GIT โ โ $ top โ
โ * main โ โ Interactive ยท F2 exit โ
โโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโโโ
โ AI Chat โ AI Input โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโผโโโโโโโโโโโโโโโโโโโโโโโโโค
โ (interactive shell takes over โ AI paused in interactiveโ
โ all keys forwarded to PTY) โ mode โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโดโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Focus:Terminal Interactive ยท Capturing Ctrl+Q quit โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Requirements
- PHP >= 8.3
- Extension: swoole (recommended, provides the coroutine runtime); falls back to a pure blocking-read mode automatically when not installed
- Dependencies: php-tui/php-tui
^0.2, scrivo/highlight.php^9.18(installed via Composer)
Installation
composer install
Running
php bin/vicecode.php # launch (uses the Swoole coroutine runtime by default) php bin/vicecode.php path/to/file # launch and open the given file directly (multiple allowed) TUI_USE_SWOOLE=0 php bin/vicecode.php # force fallback to the pure php-tui/term blocking-read mode
Must run in a real terminal (pty); size detection and raw mode are unavailable under a pipe/redirection.
General shortcuts
| Key | Action |
|---|---|
Tab |
cycle focus between panels |
Ctrl+Q |
quit the app (confirm first if there are unsaved changes) |
Esc |
exit the current modal (menu/help); quits the app when the terminal input is empty |
? |
toggle the shortcut help page |
F10 |
open/close the top menu bar |
Ctrl+T |
switch theme |
Shift+โ / Shift+โ |
horizontal scroll (covers editor/terminal/AI/sidebar) |
| mouse wheel / drag | scroll content; drag panel dividers to resize the layout (persisted) |
Clickable status bar: clicking the lang= / Theme= segment pops up the matching list above the status bar (โ/โ to move, Enter or a click to apply, Esc or a click elsewhere to close); the View โ Language / Theme menu items open the same lists, while Ctrl+T still cycles the theme quickly. Segments without a list (e.g. file=) are not clickable.
For the Editor (Enter newline, Ctrl+S save, Ctrl+W close, Ctrl+Tab switch tab, etc.), Explorer (single-click to open a file, double-click a directory to expand/collapse, click the leading triangle to expand/collapse), and GIT / Search panel usage, see the in-app ? help page.
Editor multi-cursor (edit several lines at once): Alt+โ / Alt+โ adds a cursor on the line above/below, or Alt+click adds one where you click (at most one cursor per line). Typing, backspace, Delete, Enter and Tab/Shift+Tab indentation then apply to all cursors; Esc collapses back to a single cursor (in that state Esc will not quit). The status bar shows N cursors in its file= segment. Auto-pairing and paste do not participate with multiple cursors (they act on the main cursor only) โ for auto-pairing because "should this pair here?" can differ per position, so one keystroke producing different results would be unpredictable. Alt+click adds a cursor and does not start a selection (hold Alt, otherwise it is a normal drag-copy).
Panel visibility & terminal maximize
All three side panels can be collapsed/expanded at any time. The entries live only in the View menu and the command palette (F1) โ deliberately no Ctrl shortcut:
| View menu item | Effect |
|---|---|
| Hide / Show Sidebar | collapse the left column (the centre column takes the width; Tab skips it) |
| Hide / Show AI Column | collapse the right column (stream + input together) |
| Hide / Show Terminal | collapse the bottom half of the centre column; the editor fills it |
| Maximize / Restore Terminal | the centre column holds only the terminal; both side columns stay |
Labels follow the current state ("Hide Sidebar" = currently visible); focus moves to the nearest visible panel if it sat on a panel that gets hidden; hiding the terminal also cancels maximize; all of it persists with your preferences. The status bar marks hidden:sidebar/AI/terminal (or term max) whenever something is hidden or maximized.
Terminal panel
The Terminal panel is a real PTY by default: it allocates a pseudo-terminal and launches an interactive shell (bash, reading your ~/.bashrc), forwarding all keystrokes straight to it. So aliases / functions / the prompt are all there (ll and friends just work) and you can run vim, top, less, ssh, TUI programs, or anything that needs a full terminal. A built-in lightweight VT100/ANSI emulator handles cursor positioning, coloring, erasing, scrolling, and the alternate screen.
1. Interactive terminal (default)
- Just type: focus the Terminal panel with
Tab, then start typing to take over the keyboard (no need to pressF2first) โ the first printable character or Enter enters capture mode and that key is forwarded to the shell. - Exit capture (shell keeps running in the background): press
EscorF2. Focus returns to app navigation; you can then usePageUp/PageDownand arrow keys to browse the terminal scrollback. - Recapture: press
F2again, or just keep typing. - While not capturing these keys still belong to the app:
Tabswitches panels,?opens the help page,Escquits,PageUp/ arrows browse the scrollback. Only "typing" (printable characters / Enter) is treated as taking over the keyboard โ otherwise focus would get stuck in the terminal. - End the shell: send
exitorCtrl+Dwhile capturing โ the panel falls back to Command Runner mode (leaving "Interactive terminal exited ยท F2 to re-enter" behind). - The panel title reads
Interactive ยท capturedwhile capturing, and the hint line at the bottom always says what to press.
2. Command Runner mode (fallback state after the shell exits)
Takes over once the shell ends; runs a single command like a normal command palette (via sh -c, so your shell aliases / functions are not available here):
- Type a command in the input line at the bottom and press
Enter; output streams in real time. โ/โbrowse command history;Home/Endjump to line start/end.Ctrl+Cinterrupts the running command;Ctrl+Lclears the output.PageUp/PageDownreview past output (auto-disables "stick to bottom").- To get a real shell back: press
F2.
Window size is synced to the shell via
sttyas the panel resizes; wide characters (CJK) are rendered at 2 columns.
Session persistence (opt-in)
By default the interactive shell dies when you quit ViceCode (the PTY is an OS process, killed on exit). With session persistence enabled, ViceCode saves a snapshot of the terminal on exit โ scrollback + the current main screen as plain text plus the startup working directory โ and on the next launch automatically spawns a fresh shell and replays the snapshot, so the session appears to continue.
- Off by default โ opt in by adding
"persistSession": trueto your config (~/.vicerc, or the file pointed to byVICECODE_CONFIG). - The snapshot is written to
~/.vicecode_session(next to the config, ordirname(VICECODE_CONFIG)/.vicecode_session) with0600permissions, and is consumed (deleted) after a successful restore so it is not replayed twice. - Privacy: scrollback may contain passwords / tokens typed at prompts. The file is
0600, but consider it sensitive and only enable persistence on trusted machines.
Limitations (v1, by design):
- Only the main screen is persisted; full-screen programs (
vim/top/less/ssh) run on the alternate screen and are intentionally excluded โ their frozen UI would be garbage after restart. - No colors โ plain text only.
- Only the startup working directory is restored; a
cdperformed inside the shell is not restored. - The PTY process itself cannot be serialized; what is restored is a brand-new shell with the snapshot text injected above its fresh prompt.
AI assistant (V2)
The right-hand AI panel is wired into the workspace: OpenAI- / DeepSeek-compatible endpoints plus native Claude (Anthropic Messages API) (config in config/providers.php, keys via environment variables), streamed through a curl subprocess so the UI never blocks.
-
Explicit protocol declaration: each provider sets
protocolโ'openai'(the default: any/chat/completionsendpoint, e.g. OpenAI, DeepSeek, self-hosted gateways) or'anthropic'(native Claude:POST /v1/messages,x-api-key+anthropic-versionheaders,systemas a top-level parameter, requiredmax_tokensconfigurable per provider โ 4096 by default). Omitting it meansopenai, so existing configs need no migration. A built-inanthropicentry ships with modelsclaude-opus-4-8/claude-sonnet-4-6/claude-haiku-4-5-20251001and keyANTHROPIC_API_KEY; tool calling (the Agent loop) works fully under both protocols (tool_use/tool_resultround-trips). -
Model capabilities: declare per model in
config/providers.php(toolsfor function calling,reasoning,vision,audio; custom names are accepted too). A provider-level default applies to models without their own list, and models without any declaration default totools(existing configs need no migration). Today onlytoolschanges behavior: only models declaring it get tool definitions in the request โ so a pure reasoning model (deepseek-reasoneris pre-declared as['reasoning']) is never rejected for carrying tools. The active model's capabilities show up in the AI panel's empty-state hint, and the status bar AI segment gains a "no tools" marker whentoolsis absent. -
User-level model config (no repo edits): the menu ใAI โ Edit model configโฆใ opens
~/.vicecode.providers.phpin the built-in editor (writing a commented template if it does not exist yet). Same shape asconfig/providers.php, merged per id, field by field, with new ids appended (modelsis replaced wholesale) โ so pointingopenaiat your own gateway takes one id plus the fields you want to change. Saving (Ctrl+S) hot-reloads it and keeps the current provider/model; a syntax error keeps the previous working config and says so in the status bar. -
Model strategies (switchable tiers): an
@strategiessection in the same file maps "what am I doing now" to a concrete model (e.g.planโ a reasoning model,grindโ a cheap one). Cycle them withCtrl+Rin the AI panel, or pick one directly from the menu / command palette; the status bar always shows the active tier. A strategy may declarerequires(e.g.['tools']) and a switch is refused with an explanation when the target model does not provide it โ so "fall back to the cheap model" can never silently break the agent's tools. A wrong provider/model is likewise refused (no silent fallback). Switching provider/model by hand drops the strategy name, so the status bar never lies. -
Route by task type: a strategy may declare
kinds(e.g.['unittest', 'refactor']) and is then picked automatically for matching requests. Task types come from exactly two places, neither guessed: quick actions (explain/comment/refactor/unittest) and an input prefix such as/plan rework this module(the prefix is never sent to the model; an unknown type refuses to send and lists the known ones;//escapes a literal slash). Manual wins: picking a tier by hand (Ctrl+R, the menu, or switching provider) pins it and pauses auto-routing, whileCtrl+Rincludes an "auto" stop to hand control back โ the status bar spells out who is in charge (strategy=planยทautovsstrategy=plan), and a typed task type is reported as ignored instead of silently doing nothing. -
Code context: mention files with
@path/in/projectin the input โ each reference is expanded into a syntax-highlighted fenced block before sending (paths are validated against the project root:../, absolute paths, root-escaping symlinks and binaries are rejected; per-file capai.attachMaxBytes, default 64 KB, truncated with a note). The menu AI โ Attach editor selection / Attach current file injects context too. -
Tab completion & indentation: type
@and a candidate list of project paths pops up (directories carry a trailing slash, prefix-filtered) โTabaccepts,Shift+Tabselects the previous one,Escdismisses. With no candidates,Tabindents by 4 spaces in the AI input and the editor, andShift+Taboutdents; single-line inputs (search / commit) and the other panels keepTabfor focus cycling (reverse withShift+Tab). Plugins can serve candidates too viacompletions()(same key, same popup) โ that is the landing point for AI autocomplete. -
Read-only tools (Agent loop): the model may call
list_files/read_fileinside the project. Calls run locally and the loop continues until the model answers orai.maxSteps(default 8) is reached. In-stream you see the call line (โ name(args)) and a one-line result summary (โ name(path) โ) โ file contents are never dumped into the chat. Tool results are summarized on click-copy. Setai.toolAutoRun: falseto approve each call withy/n(Esccancels). -
Context compaction: when the estimated token count exceeds
ai.compactThreshold(default 24000), the oldest history is summarized automatically (keeping the most recentai.compactKeepRecent, default 6 messages) before the real request; compaction never splits a tool_call/result pair and falls back to "no compaction" on failure. Trigger manually via AI โ Compact conversation now. -
Conversation persistence (on by default): every finished exchange is saved to
~/.vicecode_ai(0600) and restored on the next launch;Ctrl+Lclears the chat and the archive together. Disable withai.persist: false. -
Markdown rendering: finished messages render headings, lists, quotes, inline code/links/bold/italic and fenced code blocks (highlighted via scrivo). Streaming messages render as plain text until finished. Single line breaks are preserved.
-
Quick actions: menu AI โ Explain this code / Add comments / Suggest refactoring / Write unit tests (also in the
F1command palette). In the editor,Ctrl+Esends the current file (or your selection) to the AI for an explanation in a fresh conversation.
All keys and behaviors are documented in the in-app ? help page.
Configuration & Language
- Config is persisted to
~/.vicerc(JSON: layout / theme / language,persistSession, and theaisection โpersist/toolAutoRun/maxSteps/compactThreshold/compactKeepRecent/attachMaxBytes) and auto-saved on exit. On save, ViceCode merges with the existing file so hand-edited keys (likepersistSession) are preserved. - The
editorsection controls editor behaviour. Right now it only holds auto-pairing:{ "editor": { "autoPairs": ["()", "[]", "{}", "\"\"", "''"] } }Each entry is anopen+closepair. That list is the default โ it deliberately omits<>, because in PHP<is an operator and auto-closingif ($a < $b)into<>is pure noise; add it yourself if you want it. Writing[]disables auto-pairing. Save~/.vicercfrom inside the editor and the change takes effect. Behaviour: typing an opening symbol inserts its closing one and leaves the cursor in between; typing a closing symbol that already sits to the right skips over it instead of duplicating; backspace inside an empty pair deletes both; typing an opening symbol with a selection wraps the selection; quotes are not paired when they follow a letter/digit/underscore (don't,it's). - Override the config path with the
VICECODE_CONFIGenv var (used for test isolation to avoid polluting the home directory). - UI language is switched via
APP_LOCALE, defaultzh_CN,enalso available; missing keys fall back to English.
APP_LOCALE=en php bin/vicecode.php VICECODE_CONFIG=/tmp/my_vicecode.json php bin/vicecode.php
Plugins (V1)
Plugins are loaded at runtime: on startup the app scans plugins/<id>/plugin.json, reads the entry file and loads/instantiates the plugin class via require (not through composer autoload). A single failing plugin is skipped without breaking startup.
In V1 a plugin can extend the bottom status bar โ it injects custom segments that are trimmed by priority and ordered together with the built-in segments.
Built-in example: the clock plugin (plugins/clock/): shows the current time on the right of the status bar and refreshes every second (driven by the plugin's tickInterval, which makes the main loop redraw periodically).
Writing a plugin:
- Create
plugins/<id>/plugin.json:{ "id": "myplugin", "entry": "MyPlugin.php", "class": "MyPlugin" } - Implement
App\Plugin\PluginInterface:id(): stable unique id;statusSegments(App $app): return a list ofStatusSegment(key / text / priority / order);tickInterval(): return a number of seconds (e.g.1) when periodic refresh is needed, otherwisenull.
- Drop the entry file in place โ no recompilation required.
Plugin files are plain PHP; the core loader deliberately uses a runtime require, so plugins can be dropped in or removed independently without rebuilding the app.
Configuring plugins: open File โ Installed Pluginsโฆ (or the ๐งฉ Extensions tab in the sidebar, then Enter) and ViceCode opens its own editor on the plugin-only config file ~/.vicecode.plugins.json (separate from the app's own ~/.vicerc). Ctrl+S saves and hot-reloads the plugin config โ no restart, no external editor.
Full plugin developer guide (authoring / loading / segment fields / testing) is at docs/plugins.md.
Release notes: CHANGELOG.md. Root-cause and regression-test catalogue for every fixed bug: docs/BUGFIXES.md.
For full usage (step-by-step on every panel / AI / configuration / plugins) see docs/manual.md โ this README stays a feature overview.
Tests
Run everything with the bundled runner (pass/fail is decided by each test's exit code; supports name filtering and a per-test timeout):
tools/run_tests.sh # all tests, ~220s tools/run_tests.sh pty # only tests whose filename contains "pty" composer test # same thing
Do not judge results by grepping the output โ test names contain words like "Uncaught"/"Fatal", and not every test prints a PASS marker. Always rely on exit codes.
tests/ contains both headless unit tests and real-pty end-to-end acceptance tests (require a real terminal; wrapped in timeout as a safety net). Or run individual tests:
php tests/interactive_term_unit.php # Interactive PTY: emulator + pty pipe headless unit test timeout 90 php tests/pty_interactive.php # Interactive PTY: real pty end-to-end (F2/echo/Ctrl+D) php tests/session_unit.php # Session persistence: export/import + App restore (headless) timeout 120 php tests/pty_session.php # Session persistence: save on exit -> restore on restart (real pty) php tests/m6_unit.php # terminal/editor/keybinding-drift regression php tests/docs_links.php # doc link integrity (relative links + in-page anchors)
Never run
bin/vicecode.phpbare for pty acceptance; always go throughtests/pty_*.phpand setVICECODE_CONFIGto a temp file to isolate config.