Some checks failed
Deploy (Gitea) / deploy (push) Failing after 29s
- Daemon: add handle-client-config inline handler for :config-get, :config-set, :config-list, :provider-test, :provider-models - TUI cmd-config: write .env directly, send reload to daemon if connected - TUI: /config test and /config models send TCP to daemon (fallback: daemon-not-running message) - Add Test Provider and Discover Models to Ctrl+P daemon commands
368 lines
18 KiB
Org Mode
368 lines
18 KiB
Org Mode
#+TITLE: Passepartout TUI — Model
|
|
#+PROPERTY: header-args:lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp
|
|
|
|
* Model
|
|
|
|
The TUI state is a single plist accessed via ~st~ / ~(setf st)~.
|
|
All state mutation flows through event handlers in the controller.
|
|
|
|
** Contract
|
|
|
|
1. (init-state): returns a fresh state plist with ~:msgs~ list,
|
|
~:input~ buffer, ~:dirty~ flag, ~:busy~ flag, and ~:connection~ status.
|
|
2. (add-msg role content &key gate-trace): appends a message object
|
|
to the ~:messages~ vector (v0.3.3), tagged with timestamp, role,
|
|
and optional gate-trace from the daemon (v0.4.0).
|
|
3. (queue-event ev): thread-safely enqueues an event for the
|
|
reader loop. (drain-queue) returns and clears the queue.
|
|
|
|
** Package + State
|
|
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp
|
|
(defpackage :passepartout.channel-tui
|
|
(:use :cl :passepartout :usocket :bordeaux-threads)
|
|
(:export :tui-main :st :add-msg :now
|
|
:queue-event :drain-queue :init-state
|
|
:view-status :view-chat :view-input :redraw
|
|
:input-panel-top
|
|
:on-key :process-key-event :input-text :on-daemon-msg :send-daemon
|
|
:connect-daemon :disconnect-daemon
|
|
:*theme* :theme-color :theme-switch))
|
|
(in-package :passepartout.channel-tui)
|
|
|
|
(defvar *state* nil)
|
|
(defvar *event-queue* nil)
|
|
(defvar *event-lock* (bt:make-lock "tui-event-lock"))
|
|
|
|
(defvar *theme* (cl-tty.theme:make-theme)
|
|
"The active theme instance. Populated by cl-tty.theme:load-preset.
|
|
|
|
Semantic keys (all presets define these):
|
|
:user-fg, :user-bg, :user-border, :agent-border, :agent-header, :agent-fg,
|
|
:system, :input-prompt, :input-fg, :hint, :status-bg, :status-fg,
|
|
:bg, :bg-panel, :bg-element, :bg-input, :text-muted,
|
|
:dot-connected, :dot-disconnected, :error,
|
|
:tool-running, :tool-done, :tool-error,
|
|
:thinking-bg, :symbolic-border, :separator, :accent, :dim.")
|
|
|
|
(cl-tty.theme:define-preset :amber
|
|
:dark (:user-fg "#fab283" :user-bg "#1e1e1e" :user-border "#fab283"
|
|
:agent-border "#c0a080" :agent-header "#d4956a" :agent-fg "#e8e8e8"
|
|
:system "#808080"
|
|
:input-prompt "#fab283" :input-fg "#e8e8e8" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#e8e8e8"
|
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#7fd88f" :dot-disconnected "#e06c75"
|
|
:error "#e06c75"
|
|
:tool-running "#fab283" :tool-done "#7fd88f" :tool-error "#e06c75"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#707070"
|
|
:separator "#3c3c3c" :accent "#fab283" :dim "#606060")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :gold
|
|
:dark (:user-fg "#ffd700" :user-bg "#1e1e1e" :user-border "#ffd700"
|
|
:agent-border "#c0a080" :agent-header "#d4a574" :agent-fg "#e8e8e8"
|
|
:system "#808080"
|
|
:input-prompt "#ffd700" :input-fg "#e8e8e8" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#ffd700"
|
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#7fd88f" :dot-disconnected "#e06c75"
|
|
:error "#e06c75"
|
|
:tool-running "#ffd700" :tool-done "#7fd88f" :tool-error "#e06c75"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#707070"
|
|
:separator "#3c3c3c" :accent "#ffd700" :dim "#606060")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :terracotta
|
|
:dark (:user-fg "#e87a5d" :user-bg "#1e1e1e" :user-border "#e87a5d"
|
|
:agent-border "#c0a080" :agent-header "#d4956a" :agent-fg "#e0c8b0"
|
|
:system "#808080"
|
|
:input-prompt "#e87a5d" :input-fg "#e0c8b0" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#d4956a"
|
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#6cb85c" :dot-disconnected "#d94a3a"
|
|
:error "#d94a3a"
|
|
:tool-running "#e87a5d" :tool-done "#6cb85c" :tool-error "#d94a3a"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#707070"
|
|
:separator "#3c3c3c" :accent "#e87a5d" :dim "#606060")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :sepia
|
|
:dark (:user-fg "#c4a882" :user-bg "#1e1e1e" :user-border "#c4a882"
|
|
:agent-border "#c0a080" :agent-header "#b89870" :agent-fg "#d4c4a8"
|
|
:system "#808080"
|
|
:input-prompt "#c4a882" :input-fg "#d4c4a8" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#b89870"
|
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#7aac5c" :dot-disconnected "#c84a3a"
|
|
:error "#c84a3a"
|
|
:tool-running "#c4a882" :tool-done "#7aac5c" :tool-error "#c84a3a"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#707070"
|
|
:separator "#3c3c3c" :accent "#c4a882" :dim "#606060")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :nord-warm
|
|
:dark (:user-fg "#d4a574" :user-bg "#1e1e1e" :user-border "#d4a574"
|
|
:agent-border "#c0a080" :agent-header "#c49870" :agent-fg "#e0d0c0"
|
|
:system "#808080"
|
|
:input-prompt "#d08770" :input-fg "#e0d0c0" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#c8a080"
|
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#7cb860" :dot-disconnected "#d06050"
|
|
:error "#d06050"
|
|
:tool-running "#d08770" :tool-done "#7cb860" :tool-error "#d06050"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#707070"
|
|
:separator "#3c3c3c" :accent "#d4a574" :dim "#606060")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :monokai-warm
|
|
:dark (:user-fg "#e6b87d" :user-bg "#1e1e1e" :user-border "#e6b87d"
|
|
:agent-border "#c0a080" :agent-header "#d4a06a" :agent-fg "#d8c8b0"
|
|
:system "#808080"
|
|
:input-prompt "#e6b87d" :input-fg "#d8c8b0" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#cc9966"
|
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#7ab85c" :dot-disconnected "#d94a3a"
|
|
:error "#d94a3a"
|
|
:tool-running "#e6b87d" :tool-done "#7ab85c" :tool-error "#d94a3a"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#707070"
|
|
:separator "#3c3c3c" :accent "#e6b87d" :dim "#606060")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :gruvbox-warm
|
|
:dark (:user-fg "#d8a657" :user-bg "#1e1e1e" :user-border "#d8a657"
|
|
:agent-border "#c0a080" :agent-header "#c8a070" :agent-fg "#e0c8a8"
|
|
:system "#808080"
|
|
:input-prompt "#d8a657" :input-fg "#e0c8a8" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#c8a070"
|
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1e1e1e"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#7ab85c" :dot-disconnected "#d94a3a"
|
|
:error "#d94a3a"
|
|
:tool-running "#d8a657" :tool-done "#7ab85c" :tool-error "#d94a3a"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#707070"
|
|
:separator "#3c3c3c" :accent "#d8a657" :dim "#606060")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :light-amber
|
|
:dark (:user-fg "#d4a574" :user-bg "#f5f0eb" :user-border "#c4956a"
|
|
:agent-border "#c0a090" :agent-header "#b88050" :agent-fg "#3a3a3a"
|
|
:system "#606060"
|
|
:input-prompt "#c4956a" :input-fg "#3a3a3a" :hint "#a0a0a0"
|
|
:status-bg "#e8e0d8" :status-fg "#5a5a5a"
|
|
:bg "#f5f0eb" :bg-panel "#e8e0d8" :bg-element "#f0ebe5"
|
|
:bg-input "#ffffff" :text-muted "#909090"
|
|
:dot-connected "#6cb85c" :dot-disconnected "#c84a3a"
|
|
:error "#c84a3a"
|
|
:tool-running "#c4956a" :tool-done "#6cb85c" :tool-error "#c84a3a"
|
|
:thinking-bg "#e8e0d8" :symbolic-border "#a09080"
|
|
:separator "#d0c8c0" :accent "#b88050" :dim "#a0a0a0")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :catppuccin
|
|
:dark (:user-fg "#fab387" :user-bg "#1e1e2e" :user-border "#fab387"
|
|
:agent-border "#a6adc8" :agent-header "#cba6f7" :agent-fg "#cdd6f4"
|
|
:system "#808080"
|
|
:input-prompt "#fab387" :input-fg "#cdd6f4" :hint "#6c7086"
|
|
:status-bg "#181825" :status-fg "#bac2de"
|
|
:bg "#11111b" :bg-panel "#181825" :bg-element "#1e1e2e"
|
|
:bg-input "#2e2e2e" :text-muted "#6c7086"
|
|
:dot-connected "#a6e3a1" :dot-disconnected "#f38ba8"
|
|
:error "#f38ba8"
|
|
:tool-running "#fab387" :tool-done "#a6e3a1" :tool-error "#f38ba8"
|
|
:thinking-bg "#363a4f" :symbolic-border "#6c7086"
|
|
:separator "#313244" :accent "#fab387" :dim "#585b70")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :tokyonight
|
|
:dark (:user-fg "#ff9e64" :user-bg "#1a1b26" :user-border "#ff9e64"
|
|
:agent-border "#7982a8" :agent-header "#7aa2f7" :agent-fg "#a9b1d6"
|
|
:system "#808080"
|
|
:input-prompt "#ff9e64" :input-fg "#a9b1d6" :hint "#565f89"
|
|
:status-bg "#16161e" :status-fg "#9aa5ce"
|
|
:bg "#0f0f18" :bg-panel "#16161e" :bg-element "#1a1b26"
|
|
:bg-input "#2e2e2e" :text-muted "#565f89"
|
|
:dot-connected "#9ece6a" :dot-disconnected "#db4b4b"
|
|
:error "#db4b4b"
|
|
:tool-running "#ff9e64" :tool-done "#9ece6a" :tool-error "#db4b4b"
|
|
:thinking-bg "#363b54" :symbolic-border "#565f89"
|
|
:separator "#292e42" :accent "#ff9e64" :dim "#444b6a")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :dracula
|
|
:dark (:user-fg "#ff9580" :user-bg "#1e1f2b" :user-border "#ff9580"
|
|
:agent-border "#c0c0e0" :agent-header "#bd93f9" :agent-fg "#f8f8f2"
|
|
:system "#808080"
|
|
:input-prompt "#ff9580" :input-fg "#f8f8f2" :hint "#6272a4"
|
|
:status-bg "#191a24" :status-fg "#e0e0e0"
|
|
:bg "#0f101a" :bg-panel "#191a24" :bg-element "#1e1f2b"
|
|
:bg-input "#2e2e2e" :text-muted "#6272a4"
|
|
:dot-connected "#50fa7b" :dot-disconnected "#ff5555"
|
|
:error "#ff5555"
|
|
:tool-running "#ff9580" :tool-done "#50fa7b" :tool-error "#ff5555"
|
|
:thinking-bg "#3a3b50" :symbolic-border "#6272a4"
|
|
:separator "#34354a" :accent "#ff9580" :dim "#5a5b7a")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :gemini
|
|
:dark (:user-fg "#87afff" :user-bg "#1a1a1a" :user-border "#87afff"
|
|
:agent-border "#d0d0d0" :agent-header "#d7afff" :agent-fg "#ffffff"
|
|
:system "#808080"
|
|
:input-prompt "#87afff" :input-fg "#ffffff" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#afafaf"
|
|
:bg "#000000" :bg-panel "#141414" :bg-element "#1a1a1a"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#d7ffd7" :dot-disconnected "#ff87af"
|
|
:error "#ff87af"
|
|
:tool-running "#87afff" :tool-done "#d7ffd7" :tool-error "#ff87af"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#707070"
|
|
:separator "#3a3a3a" :accent "#87afff" :dim "#5f5f5f")
|
|
:light nil)
|
|
(cl-tty.theme:define-preset :mono
|
|
:dark (:user-fg "#e0e0e0" :user-bg "#1a1a1a" :user-border "#808080"
|
|
:agent-border "#a0a0a0" :agent-header "#c0c0c0" :agent-fg "#d0d0d0"
|
|
:system "#808080"
|
|
:input-prompt "#ffffff" :input-fg "#d0d0d0" :hint "#606060"
|
|
:status-bg "#141414" :status-fg "#b0b0b0"
|
|
:bg "#0a0a0a" :bg-panel "#141414" :bg-element "#1a1a1a"
|
|
:bg-input "#2e2e2e" :text-muted "#808080"
|
|
:dot-connected "#a0a0a0" :dot-disconnected "#808080"
|
|
:error "#808080"
|
|
:tool-running "#e0e0e0" :tool-done "#a0a0a0" :tool-error "#808080"
|
|
:thinking-bg "#3a3a3a" :symbolic-border "#808080"
|
|
:separator "#303030" :accent "#ffffff" :dim "#505050")
|
|
:light nil)
|
|
|
|
;; Load default theme at startup
|
|
(cl-tty.theme:load-preset *theme* :amber)
|
|
|
|
(defun theme-save ()
|
|
"Persist current theme to disk."
|
|
(let ((path (merge-pathnames ".cache/passepartout/theme.lisp"
|
|
(user-homedir-pathname))))
|
|
(ensure-directories-exist path)
|
|
(cl-tty.theme:save-theme *theme* path)))
|
|
|
|
(defun theme-load ()
|
|
"Load persisted theme from disk. Called at startup."
|
|
(let ((path (merge-pathnames ".cache/passepartout/theme.lisp"
|
|
(user-homedir-pathname))))
|
|
(unless (cl-tty.theme:load-theme *theme* path)
|
|
(cl-tty.theme:load-preset *theme* :amber))))
|
|
|
|
(defun theme-switch (name)
|
|
"Switch to a named theme preset. Returns the preset name or nil if not found."
|
|
(let ((key (intern (string-upcase (string name)) :keyword)))
|
|
(cl-tty.theme:load-preset *theme* key)
|
|
(theme-save)
|
|
(setf (st :dirty) (list t t t))
|
|
key))
|
|
|
|
(defun theme-color (role)
|
|
"Returns a hex color string for a semantic role via cl-tty.theme."
|
|
(or (cl-tty.theme:theme-color *theme* role)
|
|
"#FFFFFF"))
|
|
|
|
(defun st (key) (getf *state* key))
|
|
(defun (setf st) (val key) (setf (getf *state* key) val))
|
|
|
|
(defun init-state ()
|
|
(setf *state*
|
|
(list :running t :mode :chat :connected nil :stream nil
|
|
:input-history nil :input-hpos 0
|
|
:text-input (cl-tty.input:make-text-input
|
|
:on-submit #'handle-submit
|
|
:on-cancel #'handle-cancel
|
|
:on-tab #'handle-tab
|
|
:on-history #'handle-history)
|
|
:messages (make-array 16 :adjustable t :fill-pointer 0)
|
|
:scroll-offset 0 :busy nil
|
|
:pending-ctrl-x nil
|
|
:scroll-at-bottom t :scroll-notify nil
|
|
:streaming-text nil :url-buffer nil ; v0.7.1
|
|
:collapsed-gates nil ; v0.7.2
|
|
:search-mode nil :search-query "" ; v0.7.2
|
|
:search-matches nil :search-match-idx 0
|
|
:sidebar-mode :auto ; v0.8.0: :auto/:visible/:hidden
|
|
:sidebar-width 42 ; v0.8.0
|
|
:expand-tool-calls nil ; v0.8.0
|
|
:mcp-count 0 ; v0.8.0
|
|
:kill-ring nil ; v0.9.0
|
|
:dialog-stack nil ; v0.8.0
|
|
:minibuffer-active nil ; v0.8.0
|
|
:command-palette-active nil ; v0.8.0
|
|
:command-palette-dialog nil ; v0.8.0
|
|
:session-cost 0.0 ; v0.9.0
|
|
:daemon-version nil ; filled by handshake
|
|
:dirty (list nil nil nil))))
|
|
#+END_SRC
|
|
|
|
** Helpers
|
|
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp
|
|
(defun now ()
|
|
(multiple-value-bind (s m h) (get-decoded-time)
|
|
(declare (ignore s))
|
|
(format nil "~2,'0d:~2,'0d" h m)))
|
|
|
|
(defun add-msg (role content &key gate-trace panel)
|
|
(vector-push-extend (list :role role :content content :time (now) :gate-trace gate-trace :panel panel) (st :messages))
|
|
;; v0.7.0: notify when scrolled up and new msg arrives
|
|
(unless (st :scroll-at-bottom)
|
|
(setf (st :scroll-notify) t))
|
|
(setf (st :dirty) (list t t nil)))
|
|
#+END_SRC
|
|
|
|
** Slash Commands
|
|
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp
|
|
(defvar *slash-commands*
|
|
'((:title "/eval <expr> — Evaluate Lisp" :value "/eval")
|
|
(:title "/undo — Undo last operation" :value "/undo")
|
|
(:title "/redo — Redo last operation" :value "/redo")
|
|
(:title "/reconnect — Re-establish daemon" :value "/reconnect")
|
|
(:title "/quit — Save history and exit" :value "/quit")
|
|
(:title "/q — Quick quit" :value "/q")
|
|
(:title "/why — Show last gate trace" :value "/why")
|
|
(:title "/tags — List tag severities" :value "/tags")
|
|
(:title "/audit <id> — Inspect memory" :value "/audit")
|
|
(:title "/audit verify — Memory integrity" :value "/audit verify")
|
|
(:title "/rewind <n> — Rewind to snapshot" :value "/rewind")
|
|
(:title "/sessions — Show memory snapshots" :value "/sessions")
|
|
(:title "/resume <n> — Resume from snapshot" :value "/resume")
|
|
(:title "/theme [name] — Show/switch theme" :value "/theme")
|
|
(:title "/context — Show context summary" :value "/context")
|
|
(:title "/search <query> — Search messages" :value "/search")
|
|
(:title "/help — Show commands" :value "/help")
|
|
(:title "/help <topic> — Search manual" :value "/help "))
|
|
"Slash commands for minibuffer select-dialog.")
|
|
|
|
(defvar *daemon-commands*
|
|
'((:title "Status — Daemon health info" :value (:action :status))
|
|
(:title "Stats — Daemon statistics" :value (:action :stats))
|
|
(:title "Ping — Daemon reachability" :value (:action :ping))
|
|
(:title "Test Provider — Check connection" :value (:action :provider-test))
|
|
(:title "Discover Models — List available" :value (:action :provider-models))
|
|
(:title "Memory Snapshot — Capture state" :value (:action :memory-snapshot))
|
|
(:title "Memory Rebuild — Rebuild indices" :value (:action :memory-rebuild))
|
|
(:title "Memory Compact — Optimize storage" :value (:action :memory-compact))
|
|
(:title "Reload Config — Reload configuration" :value (:action :reload-config))
|
|
(:title "Reload Identity — Reload identity file" :value (:action :reload-identity))
|
|
(:title "List Skills — Available skills" :value (:action :list-skills))
|
|
(:title "Help — Show daemon help" :value (:action :help)))
|
|
"Daemon commands for the command palette (Ctrl+P).")
|
|
|
|
(defun all-commands ()
|
|
"Merge slash commands, daemon commands, and menu entries into one unified list."
|
|
(append *menu-entries* *slash-commands* *daemon-commands*))
|
|
|
|
(defvar *menu-entries*
|
|
'((:title "/config — LLM providers, cascade, network, folders, identity"
|
|
:value :config-menu
|
|
:action passepartout.channel-tui::show-config-main-menu))
|
|
"Special menu entries with actions (open submenus).")
|
|
#+END_SRC
|
|
|
|
** Event Queue
|
|
#+BEGIN_SRC lisp :tangle /home/user/.local/share/passepartout/lisp/channel-tui-state.lisp
|
|
(defun queue-event (ev)
|
|
(bt:with-lock-held (*event-lock*) (push ev *event-queue*)))
|
|
|
|
(defun drain-queue ()
|
|
(bt:with-lock-held (*event-lock*)
|
|
(let ((evs (nreverse *event-queue*)))
|
|
(setf *event-queue* nil) evs)))
|
|
#+END_SRC
|