mirror of
https://github.com/mark3labs/kit.git
synced 2026-06-13 19:20:06 +00:00
feat: add session management, persistence, editor text, and status bar APIs for extensions
Implement Phase 1 extension API gaps identified in the pi-mono gap analysis: - Gap 1: Session Management API (GetMessages, GetSessionPath) — read-only access to conversation history from extensions - Gap 2: Session Persistence (AppendEntry, GetEntries) — custom extension data survives across session restarts via new ExtensionDataEntry type - Gap 10: SetEditorText — extensions can pre-fill the input editor - Gap M3: Keyed Status Bar (SetStatus, RemoveStatus) — multiple extensions can place independent entries in the TUI status bar, ordered by priority
This commit is contained in:
+599
@@ -0,0 +1,599 @@
|
||||
# Kit vs Pi Extension System: Comprehensive Gap Analysis
|
||||
|
||||
> Generated: 2026-03-01
|
||||
> Source: [pi-mono extensions](https://github.com/badlogic/pi-mono/tree/main/packages/coding-agent/examples/extensions)
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Pi's extension ecosystem contains **57+ example extensions** spanning safety guards, git integration, custom providers, session management, resource discovery, and advanced UI patterns. Kit has **10 example extensions** and a solid foundation with 13 lifecycle events and a rich widget/overlay/editor system, but lacks several critical API surfaces that Pi exposes. The gaps fall into three tiers:
|
||||
|
||||
- **Critical (17 gaps)**: Missing API capabilities that block entire categories of extensions
|
||||
- **Moderate (7 gaps)**: Capabilities that exist but lack depth compared to Pi
|
||||
- **Covered (14 areas)**: Capabilities where Kit has parity or near-parity
|
||||
|
||||
---
|
||||
|
||||
## Pi Extension Inventory (57 extensions)
|
||||
|
||||
### Safety & Lifecycle (5)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `permission-gate.ts` | Confirms dangerous bash commands (rm -rf, sudo) |
|
||||
| `protected-paths.ts` | Blocks writes to protected paths (.env, .git/) |
|
||||
| `confirm-destructive.ts` | Confirms destructive session actions (clear, fork) |
|
||||
| `dirty-repo-guard.ts` | Prevents session changes with uncommitted git |
|
||||
| `sandbox/` | OS-level sandboxing via `@anthropic-ai/sandbox-runtime` |
|
||||
|
||||
### Git Integration (2)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `git-checkpoint.ts` | Git stash checkpoints per turn, restore on fork |
|
||||
| `auto-commit-on-exit.ts` | Auto-commits using assistant message as commit msg |
|
||||
|
||||
### Custom Tools (11)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `hello.ts` | Minimal custom tool |
|
||||
| `question.ts` | Agent-initiated user questions with custom UI |
|
||||
| `questionnaire.ts` | Multi-question tabbed tool |
|
||||
| `tool-override.ts` | Override built-in tools |
|
||||
| `built-in-tool-renderer.ts` | Custom compact rendering for built-in tools |
|
||||
| `minimal-mode.ts` | Override all tool rendering for minimal display |
|
||||
| `truncated-tool.ts` | Ripgrep with output truncation |
|
||||
| `antigravity-image-gen.ts` | Image generation with external API |
|
||||
| `ssh.ts` | Delegate tools to remote via SSH |
|
||||
| `subagent/` | Delegate to specialized subagents |
|
||||
| `todo.ts` | Todo list tool with state persistence |
|
||||
|
||||
### Commands & UI (25)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `preset.ts` | Named presets (model, thinking, tools, instructions) |
|
||||
| `plan-mode/` | Read-only exploration with /plan command |
|
||||
| `tools.ts` | Interactive /tools to enable/disable tools |
|
||||
| `handoff.ts` | Transfer context to new session |
|
||||
| `qna.ts` | Extract questions from response into editor |
|
||||
| `commands.ts` | /commands with introspection and tab completion |
|
||||
| `model-status.ts` | Model change notifications in status bar |
|
||||
| `send-user-message.ts` | Programmatic message injection (3 modes) |
|
||||
| `timed-confirm.ts` | Auto-dismissing dialogs with AbortSignal |
|
||||
| `rpc-demo.ts` | Full UI method catalog |
|
||||
| `modal-editor.ts` | Vim-like modal editor (full editor replacement) |
|
||||
| `rainbow-editor.ts` | Animated rainbow editor |
|
||||
| `notify.ts` | Desktop OS notifications |
|
||||
| `titlebar-spinner.ts` | Terminal title animations |
|
||||
| `summarize.ts` | Conversation summarization |
|
||||
| `custom-footer.ts` | Custom footer with git branch + token stats |
|
||||
| `custom-header.ts` | Custom ASCII art header |
|
||||
| `overlay-test.ts` | Custom overlay with Focusable components |
|
||||
| `overlay-qa-tests.ts` | Comprehensive overlay QA tests |
|
||||
| `doom-overlay/` | DOOM game running as overlay |
|
||||
| `shutdown-command.ts` | /quit command via ctx.shutdown() |
|
||||
| `reload-runtime.ts` | Hot reload extensions at runtime |
|
||||
| `interactive-shell.ts` | Full terminal takeover for vim/htop |
|
||||
| `inline-bash.ts` | !{command} expansion in prompts |
|
||||
| `snake.ts` | Snake game with custom UI |
|
||||
|
||||
### System Prompt & Compaction (4)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `pirate.ts` | System prompt append |
|
||||
| `claude-rules.ts` | Project rules loader from .claude/rules/ |
|
||||
| `custom-compaction.ts` | Custom compaction with cross-model summarization |
|
||||
| `trigger-compact.ts` | Auto-trigger compaction at token threshold |
|
||||
|
||||
### System Integration (1)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `mac-system-theme.ts` | Syncs theme with macOS dark/light mode |
|
||||
|
||||
### Resources (1)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `dynamic-resources/` | Dynamic skill/prompt/theme loading |
|
||||
|
||||
### Messages & Communication (2)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `message-renderer.ts` | Custom message rendering with expandable details |
|
||||
| `event-bus.ts` | Inter-extension pub/sub communication |
|
||||
|
||||
### Session Metadata (2)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `session-name.ts` | Name sessions for selector |
|
||||
| `bookmark.ts` | Bookmark entries with labels for /tree |
|
||||
|
||||
### Custom Providers (3)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `custom-provider-anthropic/` | Custom Anthropic provider with OAuth |
|
||||
| `custom-provider-gitlab-duo/` | GitLab Duo provider |
|
||||
| `custom-provider-qwen-cli/` | Qwen CLI provider |
|
||||
|
||||
### External Dependencies (1)
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `with-deps/` | Extension with own package.json |
|
||||
|
||||
---
|
||||
|
||||
## Kit Extension Inventory (10 extensions)
|
||||
|
||||
| Extension | Description |
|
||||
|---|---|
|
||||
| `minimal.go` | UI visibility, footer, context stats polling |
|
||||
| `widget-status.go` | Persistent widgets, OnToolResult, input commands |
|
||||
| `tool-logger.go` | Tool event logging, PrintBlock with styling |
|
||||
| `header-footer-demo.go` | Custom header/footer with slash commands |
|
||||
| `prompt-demo.go` | All 3 prompt types + chained workflow |
|
||||
| `overlay-demo.go` | Modal overlay dialogs (info, actions, markdown, scroll) |
|
||||
| `custom-editor-demo.go` | Vim-like editor interceptor |
|
||||
| `tool-renderer-demo.go` | Custom tool rendering for read/bash |
|
||||
| `subagent-widget.go` | Background subprocess agents with live widgets |
|
||||
| `kit-kit.go` | Meta-agent with parallel experts, grid widget |
|
||||
|
||||
---
|
||||
|
||||
## Gap Analysis: Critical Gaps (Missing API Capabilities)
|
||||
|
||||
### Gap 1: Session Management API
|
||||
**Pi has:** `ctx.sessionManager` with full conversation access
|
||||
- `getEntries()` / `getBranch()` -- Read conversation history
|
||||
- `getLeafEntry()` -- Current leaf entry
|
||||
- `getLabel(entryId)` / `pi.setLabel()` -- Entry metadata/labeling
|
||||
- `getSessionFile()` -- Session file path
|
||||
|
||||
**Kit has:** Nothing. Extensions cannot read conversation history.
|
||||
|
||||
**Impact:** Blocks auto-commit (needs last assistant message), git checkpoints (needs entry IDs), handoff (needs full conversation), QnA extraction, state restoration on session resume, bookmark labeling.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `GetMessages func() []MessageEntry` to `Context` returning conversation messages
|
||||
- Add `GetCurrentEntryID func() string` for session tree position
|
||||
- Add `SetLabel func(entryId, label string)` / `GetLabel func(entryId string) string`
|
||||
- Wire in `cmd/root.go` via closures reading from the session store
|
||||
|
||||
---
|
||||
|
||||
### Gap 2: Model Management API
|
||||
**Pi has:**
|
||||
- `ctx.modelRegistry.find(provider, model)` -- Look up models
|
||||
- `ctx.modelRegistry.getApiKey(model)` -- Get API keys
|
||||
- `pi.setModel(model)` -- Change active model at runtime
|
||||
- `pi.setThinkingLevel(level)` -- Set reasoning budget
|
||||
|
||||
**Kit has:** `ctx.Model string` (read-only model name)
|
||||
|
||||
**Impact:** Blocks preset system (model switching), custom compaction (cross-model calls), QnA extraction (direct LLM calls), any extension needing to invoke a different model.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `SetModel func(provider, model string) error` to `Context`
|
||||
- Add `GetAvailableModels func() []ModelInfo` returning provider/model/context info
|
||||
- Add `GetAPIKey func(provider string) string` for credential access
|
||||
- Add `SetThinkingLevel func(level string)` for reasoning budget control
|
||||
- Wire through to the existing `llm.Provider` interface
|
||||
|
||||
---
|
||||
|
||||
### Gap 3: Tool Management API
|
||||
**Pi has:**
|
||||
- `pi.getAllTools()` -- List all registered tools
|
||||
- `pi.setActiveTools(names)` -- Enable/disable specific tools
|
||||
|
||||
**Kit has:** Nothing for tool introspection or filtering.
|
||||
|
||||
**Impact:** Blocks plan-mode (restricts tools to read-only set), preset system (tool filtering), /tools interactive toggle, any policy-based tool restriction.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `GetAllTools func() []ToolInfo` to `Context` with name, description, enabled status
|
||||
- Add `SetActiveTools func(names []string)` to filter which tools the LLM can use
|
||||
- Add `IsToolEnabled func(name string) bool` for individual checks
|
||||
- Integrate with the existing tool wrapper pipeline in `wrapper.go`
|
||||
|
||||
---
|
||||
|
||||
### Gap 4: Session Lifecycle Events (Before-hooks with Cancel)
|
||||
**Pi has:**
|
||||
- `session_before_switch` -- Can cancel session switching
|
||||
- `session_before_fork` -- Can cancel forking
|
||||
- `session_switch` -- React to session changes
|
||||
|
||||
**Kit has:** `OnSessionStart` and `OnSessionShutdown` only. No before-hooks, no cancel capability, no fork/branch events.
|
||||
|
||||
**Impact:** Blocks dirty-repo-guard, confirm-destructive, git-checkpoint (restore on fork), any defensive workflow that needs to gate session operations.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `OnSessionBeforeSwitch` event with `SessionBeforeSwitchResult{Cancel bool, Reason string}`
|
||||
- Add `OnSessionBeforeFork` event with similar cancel capability
|
||||
- Add `OnSessionSwitch` event for post-switch notifications
|
||||
- Emit these from session management code before performing operations
|
||||
|
||||
---
|
||||
|
||||
### Gap 5: Compaction Events
|
||||
**Pi has:** `session_before_compact` allowing custom compaction strategies (e.g., summarize entire conversation with a cheaper model instead of truncating).
|
||||
|
||||
**Kit has:** Nothing.
|
||||
|
||||
**Impact:** Blocks custom-compaction and trigger-compact patterns. Users cannot customize how context compaction works.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `OnBeforeCompact` event with `BeforeCompactEvent{EstimatedTokens, ContextLimit int}`
|
||||
- Result type: `BeforeCompactResult{Summary *string, FirstKeptEntryID *string}`
|
||||
- If extension returns a summary, use it instead of default compaction
|
||||
- Add `TriggerCompact func()` to `Context` for manual compaction triggers
|
||||
|
||||
---
|
||||
|
||||
### Gap 6: Custom Provider Registration
|
||||
**Pi has:** `pi.registerProvider()` allowing extensions to register complete LLM providers with streaming, OAuth, and model definitions.
|
||||
|
||||
**Kit has:** No extension-facing provider registration. Providers are compiled-in via the `llm.Provider` interface.
|
||||
|
||||
**Impact:** Blocks custom-provider-anthropic, custom-provider-gitlab-duo, custom-provider-qwen-cli patterns. Users cannot add new LLM backends via extensions.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `RegisterProvider(ProviderDef)` to `API` with:
|
||||
- `Name string`, `Models []ModelDef`
|
||||
- `Stream func(model, messages, options) StreamResult`
|
||||
- This is a large undertaking. The `ProviderDef` would need to bridge to the compiled `llm.Provider` interface.
|
||||
- **Yaegi limitation:** Complex streaming interfaces may hit Yaegi's interface generation bugs. May need concrete struct wrappers.
|
||||
- **Priority:** Lower -- this is architecturally complex and has narrow use cases.
|
||||
|
||||
---
|
||||
|
||||
### Gap 7: CLI Flag Registration
|
||||
**Pi has:** `pi.registerFlag("preset", {description, type})` and `pi.getFlag("preset")` allowing extensions to add CLI flags.
|
||||
|
||||
**Kit has:** Nothing. Extensions cannot influence CLI argument parsing.
|
||||
|
||||
**Impact:** Blocks preset system (--preset flag), plan-mode (--plan flag), sandbox (--no-sandbox flag).
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `RegisterFlag(FlagDef)` to `API` with `Name, Description, Type string, Default any`
|
||||
- Add `GetFlag func(name string) any` to `Context`
|
||||
- Parse extension flags after loading extensions but before `Init()`
|
||||
- Store in a `map[string]any` on the Runner
|
||||
|
||||
---
|
||||
|
||||
### Gap 8: Keyboard Shortcut Registration
|
||||
**Pi has:** `pi.registerShortcut(Key.ctrlShift("u"), {description, handler})` for global keyboard shortcuts.
|
||||
|
||||
**Kit has:** Nothing. Only editor interceptors can handle keys, and only when the editor has focus.
|
||||
|
||||
**Impact:** Blocks global shortcuts like Ctrl+Alt+P for plan mode toggle, Ctrl+Shift+U for preset switching.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `RegisterShortcut(ShortcutDef)` to `API` with `Key string, Description string, Handler func(Context)`
|
||||
- Bridge to BubbleTea's key handling in `model.go` Update() method
|
||||
- Query registered shortcuts from Runner in the key dispatch path
|
||||
|
||||
---
|
||||
|
||||
### Gap 9: Custom Message Rendering
|
||||
**Pi has:** `pi.registerMessageRenderer(customType, renderFn)` for custom visual rendering of specific message types (not just tool results).
|
||||
|
||||
**Kit has:** `RegisterToolRenderer` for tool-specific rendering only. No general message renderer.
|
||||
|
||||
**Impact:** Blocks status-update messages, extension-branded messages, and any custom message type that needs bespoke visual treatment.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `RegisterMessageRenderer(MessageRenderConfig)` to `API` with:
|
||||
- `CustomType string` -- message type to match
|
||||
- `Render func(content, details string, expanded bool, width int) string`
|
||||
- Integrate with the stream component's message rendering pipeline
|
||||
|
||||
---
|
||||
|
||||
### Gap 10: Programmatic Editor Control
|
||||
**Pi has:**
|
||||
- `ctx.ui.setEditorText(text)` -- Pre-fill the input editor
|
||||
- `ctx.ui.setEditorComponent(factory)` -- Replace the entire editor
|
||||
|
||||
**Kit has:** `SetEditor(EditorConfig)` which is an interceptor (HandleKey/Render) but does NOT allow setting editor text or full replacement.
|
||||
|
||||
**Impact:** Blocks QnA (pre-fill editor with extracted questions), handoff (pre-fill with handoff prompt), and full editor replacement patterns.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `SetEditorText func(text string)` to `Context` -- inserts text into the active editor
|
||||
- Optionally add `SetEditorComponent func(EditorComponentConfig)` for full replacement (complex due to BubbleTea integration)
|
||||
|
||||
---
|
||||
|
||||
### Gap 11: Turn-Level Events
|
||||
**Pi has:**
|
||||
- `turn_start` -- Fires when a new LLM turn begins
|
||||
- `turn_end` -- Fires when a turn completes
|
||||
|
||||
**Kit has:** `OnAgentStart`/`OnAgentEnd` which fire at the agent loop level (may span multiple turns), and `OnMessageStart`/`OnMessageEnd` for streaming. No dedicated turn boundary events.
|
||||
|
||||
**Impact:** Blocks git-checkpoint (create stash per turn), plan-mode (track done markers per turn), preset (persist state per turn), progress tracking.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `OnTurnStart(func(TurnStartEvent, Context))` and `OnTurnEnd(func(TurnEndEvent, Context))`
|
||||
- `TurnStartEvent{TurnNumber int, Prompt string}`
|
||||
- `TurnEndEvent{TurnNumber int, Response string, StopReason string}`
|
||||
- Emit from the agent loop between turns
|
||||
|
||||
---
|
||||
|
||||
### Gap 12: Context Filtering Event
|
||||
**Pi has:** `pi.on("context", ...)` -- Lets extensions filter/modify messages before sending to the LLM. Returns `{messages: [...]}` to replace the context window.
|
||||
|
||||
**Kit has:** Nothing. Extensions cannot influence what messages the LLM sees.
|
||||
|
||||
**Impact:** Blocks plan-mode (filter stale messages), any extension needing to manage context window content, RAG-style context injection.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `OnContextPrepare(func(ContextPrepareEvent, Context) *ContextPrepareResult)`
|
||||
- `ContextPrepareEvent{Messages []MessageEntry}`
|
||||
- `ContextPrepareResult{Messages []MessageEntry}` -- return filtered/modified set
|
||||
- Emit just before sending messages to the LLM provider
|
||||
|
||||
---
|
||||
|
||||
### Gap 13: Inter-Extension Event Bus
|
||||
**Pi has:** `pi.events.on(name, handler)` / `pi.events.emit(name, data)` for decoupled inter-extension communication.
|
||||
|
||||
**Kit has:** Nothing. Extensions are isolated; they cannot communicate with each other.
|
||||
|
||||
**Impact:** Blocks coordinated multi-extension workflows (e.g., theme extension reacting to mode changes from another extension).
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `OnCustomEvent func(name string, handler func(data string))` to `API`
|
||||
- Add `EmitCustomEvent func(name, data string)` to `Context`
|
||||
- Store handlers in Runner's event map, dispatch via `Emit`
|
||||
|
||||
---
|
||||
|
||||
### Gap 14: Session Persistence for Extensions
|
||||
**Pi has:** `pi.appendEntry(customType, data)` -- Persists extension-specific data in the session journal. Survives across session resume.
|
||||
|
||||
**Kit has:** Nothing. Extension state is ephemeral (package-level vars lost on restart).
|
||||
|
||||
**Impact:** Blocks preset state restoration, plan-mode progress persistence, todo list persistence across sessions, any extension needing durable state.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `AppendEntry func(entryType string, data string)` to `Context`
|
||||
- Add `GetEntries func(entryType string) []string` to `Context` for retrieval
|
||||
- Store in session file as custom entry types
|
||||
- Emit entries during `OnSessionStart` for restoration
|
||||
|
||||
---
|
||||
|
||||
### Gap 15: Resource Discovery System
|
||||
**Pi has:** `resources_discover` event where extensions can dynamically register skills, prompts, and themes by returning file paths.
|
||||
|
||||
**Kit has:** Nothing. No concept of dynamic resource loading.
|
||||
|
||||
**Impact:** Blocks dynamic-resources pattern. Extensions cannot contribute prompts, skills, or themes at runtime.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `OnResourceDiscovery(func(ResourceDiscoveryEvent, Context) *ResourceDiscoveryResult)`
|
||||
- `ResourceDiscoveryResult{SkillPaths, PromptPaths, ThemePaths []string}`
|
||||
- Integrate with any future resource/skill loading system
|
||||
|
||||
---
|
||||
|
||||
### Gap 16: Programmatic Shutdown and Reload
|
||||
**Pi has:**
|
||||
- `ctx.shutdown()` -- Programmatically quit the application
|
||||
- `ctx.reload()` -- Hot-reload all extensions at runtime
|
||||
|
||||
**Kit has:** Neither capability.
|
||||
|
||||
**Impact:** Blocks shutdown-command, reload-runtime patterns. Extensions cannot control app lifecycle.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `Shutdown func()` to `Context` -- triggers graceful shutdown
|
||||
- Add `Reload func() error` to `Context` -- reloads all extensions
|
||||
- Wire via BubbleTea Quit msg and loader re-initialization
|
||||
|
||||
---
|
||||
|
||||
### Gap 17: Direct LLM Completion from Extensions
|
||||
**Pi has:** Extensions can call `complete()` from `@mariozechner/pi-ai` to make LLM calls outside the main agent loop (e.g., summarization, question extraction, handoff generation).
|
||||
|
||||
**Kit has:** No way for extensions to invoke LLM completions directly. Extensions can only spawn Kit subprocesses.
|
||||
|
||||
**Impact:** Blocks in-process LLM calls for summarization, QnA extraction, context transfer. The subprocess pattern works but is heavier.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `Complete func(CompleteRequest) (string, error)` to `Context`
|
||||
- `CompleteRequest{Model, SystemPrompt string, Messages []SimpleMessage}`
|
||||
- Wire through to existing `llm.Provider.Complete()` method
|
||||
- Consider rate limiting and cost awareness
|
||||
|
||||
---
|
||||
|
||||
## Gap Analysis: Moderate Gaps (Partial Coverage)
|
||||
|
||||
### Gap M1: Tool Registration Depth
|
||||
**Pi has:** `renderCall(args, theme)`, `renderResult(result, {expanded, isPartial}, theme)` directly on tool definition. Also `onUpdate` streaming callback, `AbortSignal`, and TypeBox schemas.
|
||||
|
||||
**Kit has:** Separate `RegisterToolRenderer()` and simpler `RegisterTool()` with JSON schema string and basic execute handler.
|
||||
|
||||
**Implementation approach:** Enhance `ToolDef` with optional `RenderHeader`/`RenderBody` fields. Add `onUpdate func(string)` to execute handler for streaming tool progress. Add abort/cancel context.
|
||||
|
||||
---
|
||||
|
||||
### Gap M2: Command Tab Completion
|
||||
**Pi has:** `getArgumentCompletions(prefix)` on command registration for tab-completing command arguments.
|
||||
|
||||
**Kit has:** `RegisterCommand()` without completion support.
|
||||
|
||||
**Implementation approach:** Add optional `Complete func(prefix string) []string` to `CommandDef`.
|
||||
|
||||
---
|
||||
|
||||
### Gap M3: Keyed Status Bar Entries
|
||||
**Pi has:** `ctx.ui.setStatus(key, text)` for multiple independent status bar indicators.
|
||||
|
||||
**Kit has:** `SetFooter(HeaderFooterConfig)` as a single custom footer, not keyed status entries.
|
||||
|
||||
**Implementation approach:** Add `SetStatus func(key, text string)` / `RemoveStatus func(key string)` to `Context`. Render all keyed entries in the status bar region.
|
||||
|
||||
---
|
||||
|
||||
### Gap M4: Full Custom TUI Components
|
||||
**Pi has:** `ctx.ui.custom<T>(factory)` where factory receives `(tui, theme, keybindings, done)` and returns a `Focusable` component. Supports overlays and full TUI takeover (including `tui.stop()`/`tui.start()` for subprocess terminal sharing).
|
||||
|
||||
**Kit has:** `ShowOverlay(OverlayConfig)` with text content and action buttons. No way to render completely custom interactive components or suspend the TUI.
|
||||
|
||||
**Implementation approach:**
|
||||
- This is architecturally complex with Yaegi. A simpler approach: add `SuspendTUI func(callback func())` to `Context` that stops BubbleTea, runs the callback (allowing raw terminal use), then restarts.
|
||||
- For custom overlays: enhance `OverlayConfig` with a `RenderFunc` option for custom content rendering.
|
||||
|
||||
---
|
||||
|
||||
### Gap M5: SendMessage Delivery Modes
|
||||
**Pi has:** Three modes:
|
||||
- `pi.sendUserMessage(text)` -- Normal (triggers turn)
|
||||
- `pi.sendUserMessage(text, {deliverAs: "steer"})` -- Interrupts current stream
|
||||
- `pi.sendUserMessage(text, {deliverAs: "followUp"})` -- Queues after current stream
|
||||
|
||||
**Kit has:** `ctx.SendMessage(string)` which queues if agent is busy (similar to followUp), but no steering/interrupt mode and no structured content.
|
||||
|
||||
**Implementation approach:**
|
||||
- Add `SendMessageOpts{DeliverAs string}` parameter to `SendMessage`
|
||||
- Support `"steer"` (cancel current + send) and `"followUp"` (queue) modes
|
||||
- Add `SendStructuredMessage func(content []ContentBlock, opts SendMessageOpts)` for multi-part messages
|
||||
|
||||
---
|
||||
|
||||
### Gap M6: Model Change Event
|
||||
**Pi has:** `model_select` event with `event.model`, `event.previousModel`, `event.source`.
|
||||
|
||||
**Kit has:** No model change notification.
|
||||
|
||||
**Implementation approach:** Add `OnModelChange(func(ModelChangeEvent, Context))` with `NewModel, PreviousModel, Source string`.
|
||||
|
||||
---
|
||||
|
||||
### Gap M7: User Bash Hook
|
||||
**Pi has:** `user_bash` event for intercepting user-initiated `!command` invocations, separate from tool-initiated bash. Can return custom `result` to override execution.
|
||||
|
||||
**Kit has:** No distinction between user-initiated and tool-initiated bash. `OnToolCall` catches both.
|
||||
|
||||
**Implementation approach:** Add `OnUserBash(func(UserBashEvent, Context) *UserBashResult)` or tag `ToolCallEvent` with a `Source` field (`"user"` vs `"tool"`).
|
||||
|
||||
---
|
||||
|
||||
## Capabilities with Parity (Covered)
|
||||
|
||||
| Capability | Kit | Pi | Status |
|
||||
|---|---|---|---|
|
||||
| Session start/shutdown events | `OnSessionStart`, `OnSessionShutdown` | `session_start`, `session_shutdown` | Parity |
|
||||
| Before agent start (system prompt injection) | `OnBeforeAgentStart` returns `InjectText`, `SystemPrompt` | `before_agent_start` returns `systemPrompt`, `message` | Parity |
|
||||
| Agent lifecycle events | `OnAgentStart`, `OnAgentEnd` | `agent_start`, `agent_end` | Parity |
|
||||
| Message streaming events | `OnMessageStart`, `OnMessageUpdate`, `OnMessageEnd` | N/A (Pi uses `turn_start`/`turn_end` instead) | Kit advantage |
|
||||
| Tool call interception (blocking) | `OnToolCall` returns `Block`, `Reason` | `tool_call` returns `block`, `reason` | Parity |
|
||||
| Tool result modification | `OnToolResult` returns modified `Content`, `IsError` | `tool_result` returns modified content | Parity |
|
||||
| Tool execution timing | `OnToolExecutionStart`, `OnToolExecutionEnd` | N/A | Kit advantage |
|
||||
| Input interception/transform | `OnInput` returns `Action` (continue/transform/handled) | `input` returns `action` (continue/transform/handled) | Parity |
|
||||
| Custom tool registration | `RegisterTool(ToolDef)` | `pi.registerTool({...})` | Parity (Pi richer) |
|
||||
| Custom command registration | `RegisterCommand(CommandDef)` | `pi.registerCommand(name, {...})` | Parity |
|
||||
| Widget system | `SetWidget`/`RemoveWidget` with placement, priority | `setWidget(key, lines)` | Parity |
|
||||
| Header/Footer | `SetHeader`/`SetFooter` with content/style | `setHeader`/`setFooter` with factory | Parity (different models) |
|
||||
| Overlay dialogs | `ShowOverlay` with actions, scrolling, markdown | `ctx.ui.custom({overlay: true})` | Pi richer |
|
||||
| Interactive prompts | `PromptSelect`, `PromptConfirm`, `PromptInput` | `ctx.ui.select`, `ctx.ui.confirm`, `ctx.ui.input` | Parity |
|
||||
| Editor interceptor | `SetEditor(EditorConfig)` with HandleKey/Render | `setEditorComponent()` for full replacement | Pi richer |
|
||||
| Tool renderer customization | `RegisterToolRenderer(ToolRenderConfig)` | `renderCall`/`renderResult` on tool def | Parity |
|
||||
| UI visibility control | `SetUIVisibility(UIVisibility)` | N/A (Pi uses direct component replacement) | Kit advantage |
|
||||
| Context stats | `GetContextStats()` returns tokens, limit, usage% | Token data via `sessionManager.getBranch()` | Kit advantage (dedicated API) |
|
||||
| Print functions | `Print`, `PrintInfo`, `PrintError`, `PrintBlock` | `ctx.ui.notify(msg, level)` | Different models, both adequate |
|
||||
| Subprocess spawning | `os/exec` via Yaegi stdlib access | `pi.exec()` abstracted API | Parity (different approach) |
|
||||
|
||||
---
|
||||
|
||||
## Priority-Ordered Implementation Roadmap
|
||||
|
||||
### Phase 1: High-Impact, Lower Complexity
|
||||
These gaps block the most important extension patterns and are relatively straightforward to implement.
|
||||
|
||||
1. **Session Management API** (Gap 1) -- Enables git integration, state restoration, bookmarks
|
||||
2. **Turn-Level Events** (Gap 11) -- Enables per-turn checkpoints and progress tracking
|
||||
3. **Session Persistence** (Gap 14) -- Enables durable extension state across restarts
|
||||
4. **Programmatic Editor Control** (Gap 10) -- Enables QnA and handoff patterns
|
||||
5. **Keyed Status Bar** (Gap M3) -- Enables richer status display
|
||||
|
||||
### Phase 2: Medium Impact, Medium Complexity
|
||||
6. **Tool Management API** (Gap 3) -- Enables plan-mode and tool filtering
|
||||
7. **Model Management API** (Gap 2) -- Enables presets and model switching
|
||||
8. **CLI Flag Registration** (Gap 7) -- Enables --preset, --plan flags
|
||||
9. **Inter-Extension Event Bus** (Gap 13) -- Enables cross-extension coordination
|
||||
10. **SendMessage Delivery Modes** (Gap M5) -- Enables steering and follow-up patterns
|
||||
|
||||
### Phase 3: High Impact, High Complexity
|
||||
11. **Session Lifecycle Before-Hooks** (Gap 4) -- Enables safety guards with cancel
|
||||
12. **Context Filtering Event** (Gap 12) -- Enables context management
|
||||
13. **Compaction Events** (Gap 5) -- Enables custom compaction strategies
|
||||
14. **Direct LLM Completion** (Gap 17) -- Enables in-process sub-agent calls
|
||||
15. **Full Custom TUI Components** (Gap M4) -- Enables interactive-shell, games
|
||||
|
||||
### Phase 4: Specialized / Lower Priority
|
||||
16. **Keyboard Shortcut Registration** (Gap 8) -- Nice-to-have for power users
|
||||
17. **Custom Message Rendering** (Gap 9) -- Nice-to-have for branded messages
|
||||
18. **Custom Provider Registration** (Gap 6) -- Architecturally complex, narrow use cases
|
||||
19. **Resource Discovery** (Gap 15) -- Depends on future skill/resource system
|
||||
20. **Programmatic Shutdown/Reload** (Gap 16) -- Nice-to-have lifecycle control
|
||||
21. **Model Change Event** (Gap M6) -- Nice-to-have notification
|
||||
22. **User Bash Hook** (Gap M7) -- Nice-to-have distinction
|
||||
23. **Command Tab Completion** (Gap M2) -- Nice-to-have UX improvement
|
||||
24. **Tool Registration Depth** (Gap M1) -- Incremental improvement
|
||||
|
||||
---
|
||||
|
||||
## Extension Ecosystem Gap: Example Extensions We Should Build
|
||||
|
||||
Beyond API gaps, Pi simply has more example extensions demonstrating real-world patterns. Extensions we should create (once APIs exist):
|
||||
|
||||
| Extension | Pi Equivalent | Required API Additions |
|
||||
|---|---|---|
|
||||
| Permission gate (dangerous command confirmation) | `permission-gate.ts` | None (works today with OnToolCall) |
|
||||
| Protected paths (block writes to .env, .git/) | `protected-paths.ts` | None (works today with OnToolCall) |
|
||||
| Auto-commit on exit | `auto-commit-on-exit.ts` | Gap 1 (session messages) |
|
||||
| Git checkpoints per turn | `git-checkpoint.ts` | Gaps 1, 4, 11 |
|
||||
| Desktop notifications | `notify.ts` | None (works today with OnAgentEnd + os/exec) |
|
||||
| Inline bash expansion (!{cmd}) | `inline-bash.ts` | None (works today with OnInput transform) |
|
||||
| Plan mode (read-only exploration) | `plan-mode/` | Gaps 3, 7, 11, 12, 14 |
|
||||
| Preset system | `preset.ts` | Gaps 2, 3, 7, 8, 14 |
|
||||
| Dirty repo guard | `dirty-repo-guard.ts` | Gap 4 |
|
||||
| QnA extraction | `qna.ts` | Gaps 1, 10, 17 |
|
||||
| Handoff to new session | `handoff.ts` | Gaps 1, 10, 17, 22 (newSession) |
|
||||
| Custom compaction | `custom-compaction.ts` | Gaps 2, 5 |
|
||||
| Interactive shell (vim/htop) | `interactive-shell.ts` | Gap M4 (TUI suspend) |
|
||||
| Event bus | `event-bus.ts` | Gap 13 |
|
||||
|
||||
### Extensions Buildable Today (No API Changes Needed)
|
||||
These can be built right now with Kit's existing extension API:
|
||||
|
||||
1. **Permission gate** -- Use `OnToolCall` to intercept bash with `rm -rf`, return `Block: true`
|
||||
2. **Protected paths** -- Use `OnToolCall` to check write/edit tool paths against deny-list
|
||||
3. **Desktop notifications** -- Use `OnAgentEnd` + `os/exec` for OSC 777 or `notify-send`
|
||||
4. **Inline bash expansion** -- Use `OnInput` with `Action: "transform"` to expand `!{cmd}`
|
||||
5. **Pirate mode** -- Use `OnBeforeAgentStart` to append to system prompt
|
||||
6. **Project rules loader** -- Use `OnSessionStart` to scan, `OnBeforeAgentStart` to inject
|
||||
7. **Titlebar spinner** -- Use `OnAgentStart`/`OnAgentEnd` + `os.Stdout` for OSC sequences
|
||||
8. **File trigger** -- Use `OnSessionStart` to set up `fsnotify` watcher, `SendMessage` to inject
|
||||
|
||||
---
|
||||
|
||||
## Summary Statistics
|
||||
|
||||
| Metric | Pi | Kit | Gap |
|
||||
|---|---|---|---|
|
||||
| Example extensions | 57+ | 10 | -47 |
|
||||
| Lifecycle events | 16+ | 13 | -3+ |
|
||||
| API methods on context | 35+ | 22 | -13+ |
|
||||
| Custom providers | 3 | 0 | -3 |
|
||||
| Session management APIs | 6 | 0 | -6 |
|
||||
| Model management APIs | 4 | 1 (read-only) | -3 |
|
||||
| Tool management APIs | 2 | 0 | -2 |
|
||||
| Critical API gaps | -- | -- | 17 |
|
||||
| Moderate API gaps | -- | -- | 7 |
|
||||
| Extensions buildable today | -- | -- | 8 |
|
||||
+59
-5
@@ -457,6 +457,31 @@ func footerProviderForUI(k *kit.Kit) func() *ui.WidgetData {
|
||||
}
|
||||
}
|
||||
|
||||
// statusBarProviderForUI returns a function that fetches extension status bar
|
||||
// entries and converts them to ui.StatusBarEntryData for the TUI. Returns nil
|
||||
// if extensions are disabled, which is safe — the TUI treats a nil
|
||||
// GetStatusBarEntries as "no extension entries".
|
||||
func statusBarProviderForUI(k *kit.Kit) func() []ui.StatusBarEntryData {
|
||||
if !k.HasExtensions() {
|
||||
return nil
|
||||
}
|
||||
return func() []ui.StatusBarEntryData {
|
||||
entries := k.GetExtensionStatusEntries()
|
||||
if len(entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
result := make([]ui.StatusBarEntryData, len(entries))
|
||||
for i, e := range entries {
|
||||
result[i] = ui.StatusBarEntryData{
|
||||
Key: e.Key,
|
||||
Text: e.Text,
|
||||
Priority: e.Priority,
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func runNormalMode(ctx context.Context) error {
|
||||
// Validate flag combinations
|
||||
if quietFlag && promptFlag == "" {
|
||||
@@ -685,6 +710,33 @@ func runNormalMode(ctx context.Context) error {
|
||||
kitInstance.ResetExtensionEditor()
|
||||
go appInstance.NotifyWidgetUpdate()
|
||||
},
|
||||
GetMessages: func() []extensions.SessionMessage {
|
||||
return kitInstance.GetSessionMessages()
|
||||
},
|
||||
GetSessionPath: func() string {
|
||||
return kitInstance.GetSessionFilePath()
|
||||
},
|
||||
AppendEntry: func(entryType string, data string) (string, error) {
|
||||
return kitInstance.AppendExtensionEntry(entryType, data)
|
||||
},
|
||||
GetEntries: func(entryType string) []extensions.ExtensionEntry {
|
||||
return kitInstance.GetExtensionEntries(entryType)
|
||||
},
|
||||
SetEditorText: func(text string) {
|
||||
appInstance.SetEditorTextFromExtension(text)
|
||||
},
|
||||
SetStatus: func(key string, text string, priority int) {
|
||||
kitInstance.SetExtensionStatus(extensions.StatusBarEntry{
|
||||
Key: key,
|
||||
Text: text,
|
||||
Priority: priority,
|
||||
})
|
||||
appInstance.NotifyWidgetUpdate()
|
||||
},
|
||||
RemoveStatus: func(key string) {
|
||||
kitInstance.RemoveExtensionStatus(key)
|
||||
appInstance.NotifyWidgetUpdate()
|
||||
},
|
||||
ShowOverlay: func(config extensions.OverlayConfig) extensions.OverlayResult {
|
||||
ch := make(chan app.OverlayResponse, 1)
|
||||
appInstance.SendOverlayRequest(app.OverlayRequestEvent{
|
||||
@@ -741,10 +793,11 @@ func runNormalMode(ctx context.Context) error {
|
||||
getToolRenderer := toolRendererProviderForUI(kitInstance)
|
||||
getEditorInterceptor := editorInterceptorProviderForUI(kitInstance)
|
||||
getUIVisibility := uiVisibilityProviderForUI(kitInstance)
|
||||
getStatusBarEntries := statusBarProviderForUI(kitInstance)
|
||||
|
||||
// Check if running in non-interactive mode
|
||||
if promptFlag != "" {
|
||||
return runNonInteractiveModeApp(ctx, appInstance, cli, promptFlag, quietFlag, jsonFlag, noExitFlag, modelName, parsedProvider, kitInstance.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility)
|
||||
return runNonInteractiveModeApp(ctx, appInstance, cli, promptFlag, quietFlag, jsonFlag, noExitFlag, modelName, parsedProvider, kitInstance.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries)
|
||||
}
|
||||
|
||||
// Quiet mode is not allowed in interactive mode
|
||||
@@ -752,7 +805,7 @@ func runNormalMode(ctx context.Context) error {
|
||||
return fmt.Errorf("--quiet flag can only be used with --prompt/-p")
|
||||
}
|
||||
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, modelName, parsedProvider, kitInstance.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility)
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, modelName, parsedProvider, kitInstance.GetLoadingMessage(), serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries)
|
||||
}
|
||||
|
||||
// runNonInteractiveModeApp executes a single prompt via the app layer and exits,
|
||||
@@ -765,7 +818,7 @@ func runNormalMode(ctx context.Context) error {
|
||||
//
|
||||
// When --no-exit is set, after the prompt completes the interactive BubbleTea
|
||||
// TUI is started so the user can continue the conversation.
|
||||
func runNonInteractiveModeApp(ctx context.Context, appInstance *app.App, cli *ui.CLI, prompt string, quiet, jsonOutput, noExit bool, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []ui.ExtensionCommand, contextPaths []string, skillItems []ui.SkillItem, getWidgets func(string) []ui.WidgetData, getHeader, getFooter func() *ui.WidgetData, getToolRenderer func(string) *ui.ToolRendererData, getEditorInterceptor func() *ui.EditorInterceptor, getUIVisibility func() *ui.UIVisibility) error {
|
||||
func runNonInteractiveModeApp(ctx context.Context, appInstance *app.App, cli *ui.CLI, prompt string, quiet, jsonOutput, noExit bool, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []ui.ExtensionCommand, contextPaths []string, skillItems []ui.SkillItem, getWidgets func(string) []ui.WidgetData, getHeader, getFooter func() *ui.WidgetData, getToolRenderer func(string) *ui.ToolRendererData, getEditorInterceptor func() *ui.EditorInterceptor, getUIVisibility func() *ui.UIVisibility, getStatusBarEntries func() []ui.StatusBarEntryData) error {
|
||||
if jsonOutput {
|
||||
// JSON mode: no intermediate display, structured JSON output.
|
||||
result, err := appInstance.RunOnceResult(ctx, prompt)
|
||||
@@ -803,7 +856,7 @@ func runNonInteractiveModeApp(ctx context.Context, appInstance *app.App, cli *ui
|
||||
|
||||
// If --no-exit was requested, hand off to the interactive TUI.
|
||||
if noExit {
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, modelName, providerName, loadingMessage, serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility)
|
||||
return runInteractiveModeBubbleTea(ctx, appInstance, modelName, providerName, loadingMessage, serverNames, toolNames, mcpToolCount, extensionToolCount, usageTracker, extCommands, contextPaths, skillItems, getWidgets, getHeader, getFooter, getToolRenderer, getEditorInterceptor, getUIVisibility, getStatusBarEntries)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -897,7 +950,7 @@ func writeJSONError(err error) {
|
||||
// 4. Calls program.Run() which blocks until the user quits (Ctrl+C or /quit).
|
||||
//
|
||||
// SetupCLI is not used for interactive mode; the TUI (AppModel) handles its own rendering.
|
||||
func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []ui.ExtensionCommand, contextPaths []string, skillItems []ui.SkillItem, getWidgets func(string) []ui.WidgetData, getHeader, getFooter func() *ui.WidgetData, getToolRenderer func(string) *ui.ToolRendererData, getEditorInterceptor func() *ui.EditorInterceptor, getUIVisibility func() *ui.UIVisibility) error {
|
||||
func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelName, providerName, loadingMessage string, serverNames, toolNames []string, mcpToolCount, extensionToolCount int, usageTracker *ui.UsageTracker, extCommands []ui.ExtensionCommand, contextPaths []string, skillItems []ui.SkillItem, getWidgets func(string) []ui.WidgetData, getHeader, getFooter func() *ui.WidgetData, getToolRenderer func(string) *ui.ToolRendererData, getEditorInterceptor func() *ui.EditorInterceptor, getUIVisibility func() *ui.UIVisibility, getStatusBarEntries func() []ui.StatusBarEntryData) error {
|
||||
// Determine terminal size; fall back gracefully.
|
||||
termWidth, termHeight, err := term.GetSize(int(os.Stdout.Fd()))
|
||||
if err != nil || termWidth == 0 {
|
||||
@@ -926,6 +979,7 @@ func runInteractiveModeBubbleTea(_ context.Context, appInstance *app.App, modelN
|
||||
GetToolRenderer: getToolRenderer,
|
||||
GetEditorInterceptor: getEditorInterceptor,
|
||||
GetUIVisibility: getUIVisibility,
|
||||
GetStatusBarEntries: getStatusBarEntries,
|
||||
})
|
||||
|
||||
// Print startup info to stdout before Bubble Tea takes over the screen.
|
||||
|
||||
@@ -505,6 +505,17 @@ func (a *App) PrintFromExtension(level, text string) {
|
||||
fmt.Println(text)
|
||||
}
|
||||
|
||||
// SetEditorTextFromExtension sends an EditorTextSetEvent to the TUI to
|
||||
// pre-fill the input editor. In non-interactive mode this is a no-op.
|
||||
func (a *App) SetEditorTextFromExtension(text string) {
|
||||
a.mu.Lock()
|
||||
prog := a.program
|
||||
a.mu.Unlock()
|
||||
if prog != nil {
|
||||
prog.Send(EditorTextSetEvent{Text: text})
|
||||
}
|
||||
}
|
||||
|
||||
// NotifyWidgetUpdate sends a WidgetUpdateEvent to the TUI so it re-renders
|
||||
// extension widgets. Called from the extension context's SetWidget/RemoveWidget
|
||||
// closures. In non-interactive mode this is a no-op (widgets are TUI-only).
|
||||
|
||||
@@ -118,6 +118,13 @@ type CompactErrorEvent struct {
|
||||
// from its WidgetProvider on the next render cycle.
|
||||
type WidgetUpdateEvent struct{}
|
||||
|
||||
// EditorTextSetEvent is sent when an extension calls ctx.SetEditorText to
|
||||
// pre-fill the input editor with text. The TUI handles this by setting the
|
||||
// textarea content and moving the cursor to the end.
|
||||
type EditorTextSetEvent struct {
|
||||
Text string
|
||||
}
|
||||
|
||||
// ExtensionPrintEvent is sent when an extension calls ctx.Print, ctx.PrintInfo,
|
||||
// ctx.PrintError, or ctx.PrintBlock. The TUI renders it via the appropriate
|
||||
// renderer and tea.Println (scrollback); the CLI handler uses
|
||||
|
||||
@@ -231,6 +231,130 @@ type Context struct {
|
||||
// pct := int(stats.UsagePercent * 100)
|
||||
// fmt.Sprintf("[%s%s] %d%%", strings.Repeat("#", pct/10), strings.Repeat("-", 10-pct/10), pct)
|
||||
GetContextStats func() ContextStats
|
||||
|
||||
// --- Session Management (Gap 1) ---
|
||||
|
||||
// GetMessages returns the conversation messages on the current branch,
|
||||
// ordered from root to leaf. This is a read-only view; extensions
|
||||
// cannot modify messages directly.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// msgs := ctx.GetMessages()
|
||||
// for _, m := range msgs {
|
||||
// if m.Role == "assistant" {
|
||||
// lastResponse = m.Content
|
||||
// }
|
||||
// }
|
||||
GetMessages func() []SessionMessage
|
||||
|
||||
// GetSessionPath returns the file path of the current session's JSONL
|
||||
// file. Returns empty string for in-memory (ephemeral) sessions.
|
||||
GetSessionPath func() string
|
||||
|
||||
// --- Session Persistence (Gap 2) ---
|
||||
|
||||
// AppendEntry persists custom extension data in the session tree.
|
||||
// The data survives across session restarts and can be retrieved via
|
||||
// GetEntries. Use entryType to namespace your data (e.g. "myext:state").
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// data, _ := json.Marshal(myState)
|
||||
// ctx.AppendEntry("myext:state", string(data))
|
||||
AppendEntry func(entryType string, data string) (string, error)
|
||||
|
||||
// GetEntries retrieves all persisted extension data entries matching
|
||||
// the given type on the current branch, ordered root to leaf. Pass
|
||||
// empty string to retrieve all extension data entries.
|
||||
//
|
||||
// Example — restore state on session resume:
|
||||
//
|
||||
// entries := ctx.GetEntries("myext:state")
|
||||
// if len(entries) > 0 {
|
||||
// last := entries[len(entries)-1]
|
||||
// json.Unmarshal([]byte(last.Data), &myState)
|
||||
// }
|
||||
GetEntries func(entryType string) []ExtensionEntry
|
||||
|
||||
// SetEditorText sets the text content of the input editor. This can
|
||||
// be used to pre-fill the editor with suggested text (e.g. extracted
|
||||
// questions, handoff prompts). The cursor is moved to the end.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ctx.SetEditorText("Please review the changes in src/main.go")
|
||||
SetEditorText func(text string)
|
||||
|
||||
// --- Keyed Status Bar (Gap M3) ---
|
||||
|
||||
// SetStatus places or updates a keyed entry in the TUI status bar.
|
||||
// Multiple entries from different extensions coexist; each is identified
|
||||
// by a unique key. Lower priority values render further left.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// ctx.SetStatus("myext:branch", "main", 50)
|
||||
SetStatus func(key string, text string, priority int)
|
||||
|
||||
// RemoveStatus removes a keyed status bar entry. No-op if the key
|
||||
// does not exist.
|
||||
RemoveStatus func(key string)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Session types (exposed to Yaegi — concrete structs for session access)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// SessionMessage represents a conversation message exposed to extensions.
|
||||
// This is a simplified, read-only view of the internal message structures.
|
||||
type SessionMessage struct {
|
||||
// ID is the unique entry identifier in the session tree.
|
||||
ID string
|
||||
// ParentID links this entry to its parent in the tree.
|
||||
ParentID string
|
||||
// Role is the message role: "user", "assistant", "tool", or "system".
|
||||
Role string
|
||||
// Content is the text content of the message (tool calls and results
|
||||
// are serialized as text summaries).
|
||||
Content string
|
||||
// Model is the model that generated this message (empty for user messages).
|
||||
Model string
|
||||
// Provider is the provider used (empty for user messages).
|
||||
Provider string
|
||||
// Timestamp is the RFC3339-formatted creation time.
|
||||
Timestamp string
|
||||
}
|
||||
|
||||
// ExtensionEntry represents persisted extension data stored in the session.
|
||||
// Extensions use AppendEntry to save custom state and GetEntries to retrieve
|
||||
// it on session resume.
|
||||
type ExtensionEntry struct {
|
||||
// ID is the unique entry identifier.
|
||||
ID string
|
||||
// EntryType is the extension-defined type string (e.g. "plan-mode:state").
|
||||
EntryType string
|
||||
// Data is the extension-defined payload (JSON or plain text).
|
||||
Data string
|
||||
// Timestamp is the RFC3339-formatted creation time.
|
||||
Timestamp string
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status bar types (exposed to Yaegi — concrete structs)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// StatusBarEntry represents a keyed entry in the TUI status bar. Extensions
|
||||
// can set multiple independent entries that render alongside the built-in
|
||||
// model name and token usage display.
|
||||
type StatusBarEntry struct {
|
||||
// Key uniquely identifies this entry (e.g. "myext:git-branch").
|
||||
Key string
|
||||
// Text is the rendered content shown in the status bar.
|
||||
Text string
|
||||
// Priority controls ordering. Lower values render further left.
|
||||
// Built-in entries (model, usage) have implicit priority 100-110.
|
||||
Priority int
|
||||
}
|
||||
|
||||
// PrintBlockOpts configures a custom styled block for PrintBlock.
|
||||
|
||||
@@ -12,14 +12,15 @@ import (
|
||||
// sequentially, mirroring Pi's ExtensionRunner. Handlers execute in extension
|
||||
// load order; for cancellable events the first blocking result wins.
|
||||
type Runner struct {
|
||||
extensions []LoadedExtension
|
||||
ctx Context
|
||||
widgets map[string]WidgetConfig // keyed by widget ID
|
||||
header *HeaderFooterConfig // nil = no custom header
|
||||
footer *HeaderFooterConfig // nil = no custom footer
|
||||
customEditor *EditorConfig // nil = no custom editor interceptor
|
||||
uiVisibility *UIVisibility // nil = show everything (default)
|
||||
mu sync.RWMutex
|
||||
extensions []LoadedExtension
|
||||
ctx Context
|
||||
widgets map[string]WidgetConfig // keyed by widget ID
|
||||
statusEntries map[string]StatusBarEntry // keyed by status key
|
||||
header *HeaderFooterConfig // nil = no custom header
|
||||
footer *HeaderFooterConfig // nil = no custom footer
|
||||
customEditor *EditorConfig // nil = no custom editor interceptor
|
||||
uiVisibility *UIVisibility // nil = show everything (default)
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// LoadedExtension represents a single extension that has been discovered,
|
||||
@@ -178,6 +179,45 @@ func (r *Runner) GetWidgets(placement WidgetPlacement) []WidgetConfig {
|
||||
return result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Status bar management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// SetStatusEntry places or updates a keyed status bar entry. Thread-safe.
|
||||
func (r *Runner) SetStatusEntry(entry StatusBarEntry) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.statusEntries == nil {
|
||||
r.statusEntries = make(map[string]StatusBarEntry)
|
||||
}
|
||||
r.statusEntries[entry.Key] = entry
|
||||
}
|
||||
|
||||
// RemoveStatusEntry removes a status bar entry by key. Thread-safe.
|
||||
func (r *Runner) RemoveStatusEntry(key string) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
delete(r.statusEntries, key)
|
||||
}
|
||||
|
||||
// GetStatusEntries returns all status bar entries, sorted by priority
|
||||
// (ascending). Thread-safe.
|
||||
func (r *Runner) GetStatusEntries() []StatusBarEntry {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
result := make([]StatusBarEntry, 0, len(r.statusEntries))
|
||||
for _, e := range r.statusEntries {
|
||||
result = append(result, e)
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
if result[i].Priority != result[j].Priority {
|
||||
return result[i].Priority < result[j].Priority
|
||||
}
|
||||
return result[i].Key < result[j].Key
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Header/Footer management
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -26,6 +26,13 @@ func Symbols() interp.Exports {
|
||||
"CommandDef": reflect.ValueOf((*CommandDef)(nil)),
|
||||
"PrintBlockOpts": reflect.ValueOf((*PrintBlockOpts)(nil)),
|
||||
|
||||
// Session types
|
||||
"SessionMessage": reflect.ValueOf((*SessionMessage)(nil)),
|
||||
"ExtensionEntry": reflect.ValueOf((*ExtensionEntry)(nil)),
|
||||
|
||||
// Status bar types
|
||||
"StatusBarEntry": reflect.ValueOf((*StatusBarEntry)(nil)),
|
||||
|
||||
// Widget types
|
||||
"WidgetConfig": reflect.ValueOf((*WidgetConfig)(nil)),
|
||||
"WidgetContent": reflect.ValueOf((*WidgetContent)(nil)),
|
||||
|
||||
@@ -22,6 +22,7 @@ const (
|
||||
EntryTypeBranchSummary EntryType = "branch_summary"
|
||||
EntryTypeLabel EntryType = "label"
|
||||
EntryTypeSessionInfo EntryType = "session_info"
|
||||
EntryTypeExtensionData EntryType = "extension_data"
|
||||
)
|
||||
|
||||
// CurrentVersion is the session format version for JSONL tree sessions.
|
||||
@@ -89,6 +90,14 @@ type SessionInfoEntry struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
// ExtensionDataEntry stores custom extension data in the session tree.
|
||||
// Extensions use this to persist state that survives across session restarts.
|
||||
type ExtensionDataEntry struct {
|
||||
Entry
|
||||
ExtType string `json:"ext_type"` // Extension-defined type string (e.g. "plan-mode:state")
|
||||
Data string `json:"data"` // Extension-defined data (JSON or plain text)
|
||||
}
|
||||
|
||||
// GenerateEntryID creates a unique entry identifier (16 hex chars).
|
||||
func GenerateEntryID() string {
|
||||
bytes := make([]byte, 8)
|
||||
@@ -177,6 +186,15 @@ func NewSessionInfoEntry(parentID, name string) *SessionInfoEntry {
|
||||
}
|
||||
}
|
||||
|
||||
// NewExtensionDataEntry creates an ExtensionDataEntry.
|
||||
func NewExtensionDataEntry(parentID, extType, data string) *ExtensionDataEntry {
|
||||
return &ExtensionDataEntry{
|
||||
Entry: NewEntry(EntryTypeExtensionData, parentID),
|
||||
ExtType: extType,
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
// --- JSONL marshaling helpers ---
|
||||
|
||||
// MarshalEntry serializes any entry to a JSON line (no trailing newline).
|
||||
@@ -241,6 +259,13 @@ func UnmarshalEntry(data []byte) (any, error) {
|
||||
}
|
||||
return &e, nil
|
||||
|
||||
case EntryTypeExtensionData:
|
||||
var e ExtensionDataEntry
|
||||
if err := json.Unmarshal(data, &e); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal extension_data entry: %w", err)
|
||||
}
|
||||
return &e, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown entry type: %q", env.Type)
|
||||
}
|
||||
|
||||
@@ -283,6 +283,44 @@ func (tm *TreeManager) AppendSessionInfo(name string) (string, error) {
|
||||
return entry.ID, nil
|
||||
}
|
||||
|
||||
// AppendExtensionData adds an extension data entry to the tree and persists it.
|
||||
// Extensions use this to store custom state that survives across session restarts.
|
||||
func (tm *TreeManager) AppendExtensionData(extType, data string) (string, error) {
|
||||
tm.mu.Lock()
|
||||
defer tm.mu.Unlock()
|
||||
|
||||
entry := NewExtensionDataEntry(tm.leafID, extType, data)
|
||||
if err := tm.appendAndPersist(entry); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
tm.leafID = entry.ID
|
||||
return entry.ID, nil
|
||||
}
|
||||
|
||||
// GetExtensionData returns all extension data entries matching the given type,
|
||||
// walking the current branch from root to leaf. If extType is empty, all
|
||||
// extension data entries on the branch are returned.
|
||||
func (tm *TreeManager) GetExtensionData(extType string) []*ExtensionDataEntry {
|
||||
tm.mu.RLock()
|
||||
defer tm.mu.RUnlock()
|
||||
|
||||
if tm.leafID == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
branch := tm.getBranchLocked(tm.leafID)
|
||||
var results []*ExtensionDataEntry
|
||||
for _, entry := range branch {
|
||||
if e, ok := entry.(*ExtensionDataEntry); ok {
|
||||
if extType == "" || e.ExtType == extType {
|
||||
results = append(results, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// --- Tree navigation ---
|
||||
|
||||
// Branch moves the leaf pointer to the given entry ID, creating a branch
|
||||
@@ -601,6 +639,8 @@ func (tm *TreeManager) entryID(entry any) string {
|
||||
return e.ID
|
||||
case *SessionInfoEntry:
|
||||
return e.ID
|
||||
case *ExtensionDataEntry:
|
||||
return e.ID
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
@@ -619,6 +659,8 @@ func (tm *TreeManager) entryParentID(entry any) string {
|
||||
return e.ParentID
|
||||
case *SessionInfoEntry:
|
||||
return e.ParentID
|
||||
case *ExtensionDataEntry:
|
||||
return e.ParentID
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
|
||||
+47
-6
@@ -167,6 +167,15 @@ type WidgetData struct {
|
||||
NoBorder bool
|
||||
}
|
||||
|
||||
// StatusBarEntryData represents a keyed extension entry in the TUI status bar.
|
||||
// Multiple entries from different extensions coexist, ordered by Priority
|
||||
// (lower values render further left).
|
||||
type StatusBarEntryData struct {
|
||||
Key string // unique identifier (e.g. "myext:git-branch")
|
||||
Text string // rendered content shown in the status bar
|
||||
Priority int // lower = further left; built-in entries use 100-110
|
||||
}
|
||||
|
||||
// UIVisibility controls which built-in TUI chrome elements are visible.
|
||||
// The zero value shows everything (backward compatible).
|
||||
type UIVisibility struct {
|
||||
@@ -257,6 +266,12 @@ type AppModelOptions struct {
|
||||
// Called during View() and PrintStartupInfo() to conditionally hide
|
||||
// built-in chrome elements. May be nil if no extensions are loaded.
|
||||
GetUIVisibility func() *UIVisibility
|
||||
|
||||
// GetStatusBarEntries returns extension-provided status bar entries,
|
||||
// sorted by priority. Called during renderStatusBar() to inject
|
||||
// extension entries alongside the built-in model/usage display.
|
||||
// May be nil if no extensions are loaded.
|
||||
GetStatusBarEntries func() []StatusBarEntryData
|
||||
}
|
||||
|
||||
// AppModel is the root Bubble Tea model for the interactive TUI. It owns the
|
||||
@@ -367,6 +382,9 @@ type AppModel struct {
|
||||
// getUIVisibility returns extension-provided UI visibility overrides. May be nil.
|
||||
getUIVisibility func() *UIVisibility
|
||||
|
||||
// getStatusBarEntries returns extension-provided status bar entries. May be nil.
|
||||
getStatusBarEntries func() []StatusBarEntryData
|
||||
|
||||
// prompt holds the state of an active interactive prompt overlay. Nil
|
||||
// when no prompt is active. Managed by updatePromptState().
|
||||
prompt *promptOverlay
|
||||
@@ -481,6 +499,7 @@ func NewAppModel(appCtrl AppController, opts AppModelOptions) *AppModel {
|
||||
m.getFooter = opts.GetFooter
|
||||
m.getEditorInterceptor = opts.GetEditorInterceptor
|
||||
m.getUIVisibility = opts.GetUIVisibility
|
||||
m.getStatusBarEntries = opts.GetStatusBarEntries
|
||||
|
||||
// Store context/skills metadata and tool counts for startup display.
|
||||
m.contextPaths = opts.ContextPaths
|
||||
@@ -970,6 +989,13 @@ func (m *AppModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
// latest widget state on the next render.
|
||||
m.distributeHeight()
|
||||
|
||||
case app.EditorTextSetEvent:
|
||||
// Extension wants to pre-fill the input editor with text.
|
||||
if ic, ok := m.input.(*InputComponent); ok {
|
||||
ic.textarea.SetValue(msg.Text)
|
||||
ic.textarea.CursorEnd()
|
||||
}
|
||||
|
||||
case app.PromptRequestEvent:
|
||||
// Extension wants to show an interactive prompt. Enter prompt state.
|
||||
// If already in prompt state (concurrent prompt from another
|
||||
@@ -1176,7 +1202,8 @@ func (m *AppModel) renderStream() string {
|
||||
}
|
||||
|
||||
// renderStatusBar renders a persistent single-line status bar below the input.
|
||||
// Left side: spinner (when active). Right side: provider · model + usage stats.
|
||||
// Left side: spinner (when active). Middle: extension status entries (sorted by
|
||||
// priority). Right side: provider · model + usage stats.
|
||||
// This bar is always present so its height is constant, eliminating layout
|
||||
// shifts from spinner or usage info appearing/disappearing.
|
||||
func (m *AppModel) renderStatusBar() string {
|
||||
@@ -1187,7 +1214,21 @@ func (m *AppModel) renderStatusBar() string {
|
||||
if m.stream != nil {
|
||||
leftSide = m.stream.SpinnerView()
|
||||
}
|
||||
leftWidth := lipgloss.Width(leftSide)
|
||||
|
||||
// Middle: extension status bar entries (sorted by priority).
|
||||
var middleParts []string
|
||||
if m.getStatusBarEntries != nil {
|
||||
entries := m.getStatusBarEntries()
|
||||
for _, e := range entries {
|
||||
middleParts = append(middleParts, lipgloss.NewStyle().
|
||||
Foreground(theme.Muted).
|
||||
Render(e.Text))
|
||||
}
|
||||
}
|
||||
middleSide := strings.Join(middleParts, " ")
|
||||
if middleSide != "" && leftSide != "" {
|
||||
middleSide = " " + middleSide
|
||||
}
|
||||
|
||||
// Right side: provider · model + usage stats.
|
||||
var rightParts []string
|
||||
@@ -1211,12 +1252,12 @@ func (m *AppModel) renderStatusBar() string {
|
||||
}
|
||||
|
||||
rightSide := strings.Join(rightParts, " ")
|
||||
rightWidth := lipgloss.Width(rightSide)
|
||||
|
||||
// Fill the gap between left and right with spaces.
|
||||
gap := max(m.width-leftWidth-rightWidth, 1)
|
||||
// Fill the gap between left+middle and right with spaces.
|
||||
usedWidth := lipgloss.Width(leftSide) + lipgloss.Width(middleSide) + lipgloss.Width(rightSide)
|
||||
gap := max(m.width-usedWidth, 1)
|
||||
|
||||
return leftSide + strings.Repeat(" ", gap) + rightSide
|
||||
return leftSide + middleSide + strings.Repeat(" ", gap) + rightSide
|
||||
}
|
||||
|
||||
// renderSeparator renders the separator line with an optional queue count badge.
|
||||
|
||||
+101
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/mark3labs/kit/internal/config"
|
||||
"github.com/mark3labs/kit/internal/extensions"
|
||||
"github.com/mark3labs/kit/internal/kitsetup"
|
||||
"github.com/mark3labs/kit/internal/message"
|
||||
"github.com/mark3labs/kit/internal/session"
|
||||
"github.com/mark3labs/kit/internal/skills"
|
||||
"github.com/mark3labs/kit/internal/tools"
|
||||
@@ -288,6 +289,106 @@ func (m *Kit) GetExtensionUIVisibility() *extensions.UIVisibility {
|
||||
return m.extRunner.GetUIVisibility()
|
||||
}
|
||||
|
||||
// GetSessionMessages returns the conversation messages on the current branch
|
||||
// as extension-facing SessionMessage structs, ordered root to leaf.
|
||||
func (m *Kit) GetSessionMessages() []extensions.SessionMessage {
|
||||
if m.treeSession == nil {
|
||||
return nil
|
||||
}
|
||||
branch := m.treeSession.GetBranch("")
|
||||
var msgs []extensions.SessionMessage
|
||||
for _, entry := range branch {
|
||||
me, ok := entry.(*session.MessageEntry)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
msg, err := me.ToMessage()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
// Flatten content parts into a single text string.
|
||||
var content string
|
||||
for _, p := range msg.Parts {
|
||||
switch pt := p.(type) {
|
||||
case message.TextContent:
|
||||
content += pt.Text
|
||||
case message.ReasoningContent:
|
||||
content += pt.Thinking
|
||||
case message.ToolCall:
|
||||
content += fmt.Sprintf("[tool_call: %s(%s)]", pt.Name, pt.Input)
|
||||
case message.ToolResult:
|
||||
content += fmt.Sprintf("[tool_result: %s]", pt.Content)
|
||||
}
|
||||
}
|
||||
msgs = append(msgs, extensions.SessionMessage{
|
||||
ID: me.ID,
|
||||
ParentID: me.ParentID,
|
||||
Role: string(msg.Role),
|
||||
Content: content,
|
||||
Model: msg.Model,
|
||||
Provider: msg.Provider,
|
||||
Timestamp: me.Timestamp.Format("2006-01-02T15:04:05Z07:00"),
|
||||
})
|
||||
}
|
||||
return msgs
|
||||
}
|
||||
|
||||
// GetSessionFilePath returns the JSONL file path of the current session.
|
||||
func (m *Kit) GetSessionFilePath() string {
|
||||
if m.treeSession == nil {
|
||||
return ""
|
||||
}
|
||||
return m.treeSession.GetFilePath()
|
||||
}
|
||||
|
||||
// AppendExtensionEntry persists custom extension data in the session tree.
|
||||
func (m *Kit) AppendExtensionEntry(extType, data string) (string, error) {
|
||||
if m.treeSession == nil {
|
||||
return "", fmt.Errorf("no session available")
|
||||
}
|
||||
return m.treeSession.AppendExtensionData(extType, data)
|
||||
}
|
||||
|
||||
// GetExtensionEntries retrieves persisted extension data entries for a type.
|
||||
func (m *Kit) GetExtensionEntries(extType string) []extensions.ExtensionEntry {
|
||||
if m.treeSession == nil {
|
||||
return nil
|
||||
}
|
||||
entries := m.treeSession.GetExtensionData(extType)
|
||||
result := make([]extensions.ExtensionEntry, 0, len(entries))
|
||||
for _, e := range entries {
|
||||
result = append(result, extensions.ExtensionEntry{
|
||||
ID: e.ID,
|
||||
EntryType: e.ExtType,
|
||||
Data: e.Data,
|
||||
Timestamp: e.Timestamp.Format("2006-01-02T15:04:05Z07:00"),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// SetExtensionStatus places or updates a keyed status bar entry.
|
||||
func (m *Kit) SetExtensionStatus(entry extensions.StatusBarEntry) {
|
||||
if m.extRunner != nil {
|
||||
m.extRunner.SetStatusEntry(entry)
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveExtensionStatus removes a keyed status bar entry.
|
||||
func (m *Kit) RemoveExtensionStatus(key string) {
|
||||
if m.extRunner != nil {
|
||||
m.extRunner.RemoveStatusEntry(key)
|
||||
}
|
||||
}
|
||||
|
||||
// GetExtensionStatusEntries returns all extension status bar entries sorted by priority.
|
||||
func (m *Kit) GetExtensionStatusEntries() []extensions.StatusBarEntry {
|
||||
if m.extRunner == nil {
|
||||
return nil
|
||||
}
|
||||
return m.extRunner.GetStatusEntries()
|
||||
}
|
||||
|
||||
// HasExtensions returns true if the extension runner is configured and active.
|
||||
func (m *Kit) HasExtensions() bool {
|
||||
return m.extRunner != nil
|
||||
|
||||
Reference in New Issue
Block a user