@pellux/goodvibes-tui 1.20.0 → 1.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/README.md +7 -7
- package/docs/foundation-artifacts/operator-contract.json +1 -1
- package/package.json +7 -7
- package/src/cli/bundle-command.ts +1 -1
- package/src/cli/management.ts +1 -1
- package/src/core/active-model-identity.ts +153 -0
- package/src/core/conversation-user-receipts.ts +42 -0
- package/src/core/conversation.ts +20 -17
- package/src/core/stream-event-wiring.ts +174 -14
- package/src/core/system-message-router.ts +22 -0
- package/src/main.ts +20 -19
- package/src/{cli → providers}/provider-classification.ts +12 -0
- package/src/renderer/header-line.ts +146 -0
- package/src/renderer/shell-surface.ts +36 -0
- package/src/renderer/ui-factory.ts +25 -78
- package/src/runtime/recovery-decisions.ts +126 -0
- package/src/runtime/recovery-prompt.ts +56 -6
- package/src/version.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,13 @@ All notable changes to GoodVibes TUI.
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
+
## [1.20.1] - 2026-07-25
|
|
8
|
+
|
|
9
|
+
### Changes
|
|
10
|
+
|
|
11
|
+
- fd3b3037 fix: recovery offers and removals behave; active-model surfaces agree; failover is visible and temporary
|
|
12
|
+
- 08237438 docs: replace README screenshots with true terminal captures
|
|
13
|
+
|
|
7
14
|
## [1.20.0] - 2026-07-24
|
|
8
15
|
|
|
9
16
|
### Changes
|
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/mgd34msu/goodvibes-tui/actions/workflows/ci.yml)
|
|
4
4
|
[](https://opensource.org/licenses/MIT)
|
|
5
|
-
[](https://github.com/mgd34msu/goodvibes-tui)
|
|
6
6
|
|
|
7
7
|
GoodVibes is a terminal console for coding and operations work with an AI model. You run `goodvibes` in a project directory and get a full-screen terminal app where you talk to a model that can read and edit your files, run shell commands, search the web, and hand work off to background agents — asking your permission before anything that writes or executes. It talks to many model providers (OpenAI, Anthropic, Gemini, Bedrock, Copilot, OpenRouter and other OpenAI-compatible gateways, plus local servers like Ollama and LM Studio that it finds on startup), keeps its settings, sessions, and secrets on your own machine, and shows you the token count and running cost of every turn. Alongside the conversation, panels turn background work — running agents, git state, diffs, tokens, cost — into live control rooms.
|
|
8
8
|
|
|
9
|
-
<img src="docs/assets/splash.png" alt="GoodVibes starting up in a terminal: a glitch-art GOODVIBES wordmark in cyan-to-magenta gradient, the version and tagline beneath it, the active model and tool count, the working directory, and a hint line reading Ctrl+P panels / ? help / F2 fleet. A status footer shows mode, token usage, context usage, and the daemon and web listener addresses." width="900">
|
|
9
|
+
<img src="docs/assets/splash.png" alt="GoodVibes starting up in a terminal: a glitch-art GOODVIBES wordmark in cyan-to-magenta gradient, the version and tagline beneath it, the active model and tool count, the working directory, a line offering to resume the last session, and a hint line reading Ctrl+P panels / ? help / F2 fleet. A status footer shows mode, token usage, context usage, and the daemon and web listener addresses." width="900">
|
|
10
10
|
|
|
11
11
|
---
|
|
12
12
|
|
|
@@ -45,7 +45,7 @@ Deeper install notes — the installer's environment variables, uninstall, daemo
|
|
|
45
45
|
|
|
46
46
|
### The conversation loop
|
|
47
47
|
|
|
48
|
-
<img src="docs/assets/conversation.png" alt="
|
|
48
|
+
<img src="docs/assets/conversation.png" alt="Two conversation turns in the terminal. The tail of an earlier answer describes the project as a minimal ISS pass tracker whose calculation is still a stub. Below it the user asks the model to grep for nextPass and list every file that references it; an assistant block shows one find tool call with a green checkmark, and its result is expanded as syntax-highlighted JSON with line numbers listing the two matching file paths and a count of 2. The final answer names src/passes.ts and src/index.ts as a bulleted list. The footer reads Token Usage: Input em dash, Output 264, Cache Read 0, Cache Write 0, Total 264, approximately 0.0040 dollars, above a green context usage bar at 4 percent." width="900">
|
|
49
49
|
|
|
50
50
|
Tool calls stream inline as the model makes them, then their results fold into a collapsible group so a long working session stays readable — expand any group to see what actually came back. Assistant messages render markdown, syntax-highlighted code, and inline diffs, and any block can be collapsed, bookmarked, copied, or saved to a file.
|
|
51
51
|
|
|
@@ -57,13 +57,13 @@ The footer keeps an honest running total: fresh input tokens separated from cach
|
|
|
57
57
|
|
|
58
58
|
The first time you open a directory, GoodVibes asks how much it is allowed to do there. Restricted is read-only: the model can look around, but writes and commands are refused until you trust the workspace.
|
|
59
59
|
|
|
60
|
-
<img src="docs/assets/permission-prompt.png" alt="An execution approval prompt. A header reads EXECUTE, Shell Execution Approval. Fields list the requesting session, the tool exec, the full command, the working directory, a Risk line reading HIGH write in red, the surface and radius, a summary, the effects: process execution, filesystem mutation, possible network access,
|
|
60
|
+
<img src="docs/assets/permission-prompt.png" alt="An execution approval prompt. A header reads EXECUTE, Shell Execution Approval, one more waiting. Fields list the requesting session, the tool exec, the full command mkdir -p build, the working directory, a Risk line reading HIGH write in red, the surface and radius, a summary, the decision class, the effects: process execution, filesystem mutation, possible network access, a reviewer checklist, and the raw tool arguments. Below, four numbered remember options scope an approval to this exact command, to every mkdir command, to every exec call in this project, or to the rest of the session in memory only. The action line reads: Y Allow once, 1-4 Allow and remember, N Deny, or type a reason to deny." width="900">
|
|
61
61
|
|
|
62
62
|
After that, the default permission mode is `prompt`: writes, edits, shell commands, network fetches, agent spawns, and MCP calls each stop and ask. The prompt shows what will run, in which directory, its assessed risk, and what it can affect. You can allow it once, deny it (optionally typing a reason the model sees), or remember the decision at whichever scope fits — this exact command, this command shape, this tool for the whole project, or just for the rest of the session. Four other modes are available when prompting is not what you want: `accept-edits` auto-approves file writes and edits while exec and the other risky classes still ask, `plan` allows read-only tools and refuses every mutating or exec call, `allow-all` approves everything, and `custom` takes per-tool `allow` / `prompt` / `deny` overrides. `Shift+Tab` cycles the four session postures — normal, accept-edits, plan, auto — and `/plan` toggles plan mode directly.
|
|
63
63
|
|
|
64
64
|
### Models and providers
|
|
65
65
|
|
|
66
|
-
<img src="docs/assets/model-picker.png" alt="A fullscreen model workspace. A left column lists routing targets: Main Chat set, Helper Model off, Tool LLM off, TTS LLM inherit, and Embeddings set. The right pane shows the selected target with its current model and a filter row for search, price, capability, grouping, and availability. Below, a table of models lists model key, display name, provider, context window, tier, and capability flags across
|
|
66
|
+
<img src="docs/assets/model-picker.png" alt="A fullscreen model workspace. A left column lists routing targets: Main Chat set, Helper Model off, Tool LLM off, TTS LLM inherit, and Embeddings set. The right pane shows the selected target with its current model and a filter row for search, price, capability, grouping, and availability. Below, a table of models lists model key, display name, provider, context window, tier, and capability flags across 2030 catalogued entries, with a footer of list shortcuts." width="900">
|
|
67
67
|
|
|
68
68
|
Models come from a live catalog, so the picker lists far more than a hardcoded set, filterable by search, price, capability, and availability. Separate roles route independently: your main chat model, a cheaper helper model for grunt work, a tool model, a TTS model, and an embeddings model can each point somewhere different.
|
|
69
69
|
|
|
@@ -71,13 +71,13 @@ The `synthetic` provider groups the same model across every backend that serves
|
|
|
71
71
|
|
|
72
72
|
### The Fleet control room
|
|
73
73
|
|
|
74
|
-
<img src="docs/assets/fleet-panel.png" alt="The Fleet panel open beside the conversation. It lists background work as rows with a kind, a name, elapsed time, token count, and cost — a CI watch poller and a runtime
|
|
74
|
+
<img src="docs/assets/fleet-panel.png" alt="The Fleet panel open beside the conversation. It lists background work as rows with a kind, a name, elapsed time, token count, and cost — a CI watch poller and a runtime heartbeat watcher at 4 minutes 23 seconds each, and an idle code indexer. The detail region below shows the selected watcher's id, state idle, elapsed time, model, tokens, cost, a headline reading CI watch poller, healthy, an activity phase, and an approvals line. The action hints read j/k navigate, Enter attach, K kill, A archive finished, v archived, n host agent." width="900">
|
|
75
75
|
|
|
76
76
|
Work that is not conversation goes into panels instead of scrolling past in the transcript. Fleet — reached with `F2` — is the live control room for agents, workstreams, watchers, and scheduled jobs: what is running, for how long, at what token cost, and what it is waiting on. You can attach to a running agent to watch or steer it, detach again without killing it, pause and resume, and archive finished work. Git, diff, cost, token, and local-auth panels sit alongside it; heavier operator surfaces open as review modals.
|
|
77
77
|
|
|
78
78
|
### Everything else is a command
|
|
79
79
|
|
|
80
|
-
<img src="docs/assets/help-overlay.png" alt="A searchable help overlay titled Help, Commands, with a search field and results grouped by category. Under Provider Accounts is the /accounts command with review, panel, show, routes, and repair subcommands and a one-line description. Under Fleet is /agents, described as hosting third-party coding agents as fleet rows over ACP. A line reads
|
|
80
|
+
<img src="docs/assets/help-overlay.png" alt="A searchable help overlay titled Help, Commands, with a search field and results grouped by category. Under Provider Accounts is the /accounts command with review, panel, show, routes, and repair subcommands and a one-line description. Under Fleet is /agents, described as hosting third-party coding agents as fleet rows over ACP. Under Experience is /approval, for reviewing action-specific approval classes. A line reads 147 below, and the hints read Up/Down Navigate, Enter Run, / Search, Esc Close." width="820">
|
|
81
81
|
|
|
82
82
|
Press `?` for a searchable, categorized list of every slash command with its arguments and description. The same list is generated into [docs/commands-reference.md](docs/commands-reference.md).
|
|
83
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pellux/goodvibes-tui",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.1",
|
|
4
4
|
"description": "Terminal-native GoodVibes product for coding, operations, automation, knowledge, channels, and daemon-backed control-plane workflows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/main.ts",
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
"@anthropic-ai/vertex-sdk": "^0.16.0",
|
|
105
105
|
"@ast-grep/napi": "^0.42.0",
|
|
106
106
|
"@aws/bedrock-token-generator": "^1.1.0",
|
|
107
|
-
"@pellux/goodvibes-sdk": "1.12.
|
|
108
|
-
"@pellux/goodvibes-terminal-shell": "1.12.
|
|
107
|
+
"@pellux/goodvibes-sdk": "1.12.1",
|
|
108
|
+
"@pellux/goodvibes-terminal-shell": "1.12.1",
|
|
109
109
|
"bash-language-server": "^5.6.0",
|
|
110
110
|
"fuse.js": "^7.1.0",
|
|
111
111
|
"graphql": "^16.13.2",
|
|
@@ -131,10 +131,10 @@
|
|
|
131
131
|
"typescript": "^5.9.3"
|
|
132
132
|
},
|
|
133
133
|
"optionalDependencies": {
|
|
134
|
-
"@pellux/goodvibes-tui-linux-x64": "1.20.
|
|
135
|
-
"@pellux/goodvibes-tui-linux-arm64": "1.20.
|
|
136
|
-
"@pellux/goodvibes-tui-darwin-x64": "1.20.
|
|
137
|
-
"@pellux/goodvibes-tui-darwin-arm64": "1.20.
|
|
134
|
+
"@pellux/goodvibes-tui-linux-x64": "1.20.1",
|
|
135
|
+
"@pellux/goodvibes-tui-linux-arm64": "1.20.1",
|
|
136
|
+
"@pellux/goodvibes-tui-darwin-x64": "1.20.1",
|
|
137
|
+
"@pellux/goodvibes-tui-darwin-arm64": "1.20.1"
|
|
138
138
|
},
|
|
139
139
|
"trustedDependencies": [],
|
|
140
140
|
"overrides": {
|
|
@@ -12,7 +12,7 @@ import type { ConfigKey } from '../config/index.ts';
|
|
|
12
12
|
import type { CliCommandRuntime } from './types.ts';
|
|
13
13
|
import type { CliCommandOutput } from './types.ts';
|
|
14
14
|
import { getPackageVersion } from './help.ts';
|
|
15
|
-
import { classifyProviderSetup } from '
|
|
15
|
+
import { classifyProviderSetup } from '../providers/provider-classification.ts';
|
|
16
16
|
import { buildCliServicePosture } from './service-posture.ts';
|
|
17
17
|
import { REDACTED_VALUE, collectSensitiveConfigValues, isRedactedValue, redactConfig, redactSerializedSecrets } from './redaction.ts';
|
|
18
18
|
import { summarizeError } from '@pellux/goodvibes-sdk/platform/utils';
|
package/src/cli/management.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { beginOpenAICodexLogin, exchangeOpenAICodexCode } from '@pellux/goodvibe
|
|
|
13
13
|
import { inspectProviderAuth } from '@/runtime/index.ts';
|
|
14
14
|
import type { CliCommandRuntime, GoodVibesCliParseResult } from './types.ts';
|
|
15
15
|
import { formatProviderAuthRoute, summarizeProviderAuthRoutes } from './provider-auth-routes.ts';
|
|
16
|
-
import { classifyProviderSetup } from '
|
|
16
|
+
import { classifyProviderSetup } from '../providers/provider-classification.ts';
|
|
17
17
|
import { resolveRuntimeEndpointBinding } from './endpoints.ts';
|
|
18
18
|
import { applyRuntimeEndpointFlagOverrides } from './config-overrides.ts';
|
|
19
19
|
import type { RuntimeEndpointId } from './endpoints.ts';
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* active-model-identity.ts — the single answer to "which backend is serving
|
|
3
|
+
* this session right now, and is that what the user chose?"
|
|
4
|
+
*
|
|
5
|
+
* Two shell surfaces used to answer that question independently and could
|
|
6
|
+
* therefore contradict each other:
|
|
7
|
+
* - the top-right header read `providerRegistry.getCurrentModel()` live
|
|
8
|
+
* every frame, so it followed an automatic failover switch immediately;
|
|
9
|
+
* - the footer read `runtime.model` / `runtime.provider`, which only
|
|
10
|
+
* bootstrap and explicit user model switches ever wrote, so it kept
|
|
11
|
+
* naming the configured backend after failover had moved serving (and
|
|
12
|
+
* billing) somewhere else.
|
|
13
|
+
*
|
|
14
|
+
* Both surfaces now resolve through resolveActiveModelDisplay() so they are
|
|
15
|
+
* computed from one comparison of serving-vs-configured. Divergence is
|
|
16
|
+
* detected structurally — by comparing the live registry key against the
|
|
17
|
+
* configured selection — not by trusting a flag, so ANY path that moves the
|
|
18
|
+
* registry off the user's choice is surfaced, not just the failover path that
|
|
19
|
+
* sets the record below.
|
|
20
|
+
*
|
|
21
|
+
* The failover record adds the REASON for a divergence ("failover from …");
|
|
22
|
+
* it never decides whether one exists.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The live per-turn record of an automatic provider failover.
|
|
27
|
+
*
|
|
28
|
+
* `configuredRegistryKey` is sticky across repeated failovers within one turn
|
|
29
|
+
* (a second hop still reports divergence from what the USER chose, not from
|
|
30
|
+
* the first fallback). `servingRegistryKey` tracks the most recent switch.
|
|
31
|
+
*/
|
|
32
|
+
export interface FailoverTurnRecord {
|
|
33
|
+
/** The user's configured selection (`provider.model`) when failover began. */
|
|
34
|
+
readonly configuredRegistryKey: string;
|
|
35
|
+
/** The registry key failover switched serving to. */
|
|
36
|
+
readonly servingRegistryKey: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Mutable holder for the failover record, owned by the shell composition root
|
|
41
|
+
* (main.ts) and shared between the stream-event wiring that sets it and the
|
|
42
|
+
* render frame that reads it.
|
|
43
|
+
*/
|
|
44
|
+
export interface FailoverTurnState {
|
|
45
|
+
/** The active record, or null when no failover is in effect. */
|
|
46
|
+
current(): FailoverTurnRecord | null;
|
|
47
|
+
/**
|
|
48
|
+
* Record a failover switch. On a second switch within the same turn the
|
|
49
|
+
* originally-configured key is preserved — divergence is always reported
|
|
50
|
+
* against the user's own selection.
|
|
51
|
+
*/
|
|
52
|
+
begin(record: FailoverTurnRecord): void;
|
|
53
|
+
/** Drop the record — called only after serving is actually back on the configured selection. */
|
|
54
|
+
clear(): void;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Create an empty FailoverTurnState. */
|
|
58
|
+
export function createFailoverTurnState(): FailoverTurnState {
|
|
59
|
+
let active: FailoverTurnRecord | null = null;
|
|
60
|
+
return {
|
|
61
|
+
current: () => active,
|
|
62
|
+
begin(record: FailoverTurnRecord): void {
|
|
63
|
+
active = active === null
|
|
64
|
+
? record
|
|
65
|
+
: { configuredRegistryKey: active.configuredRegistryKey, servingRegistryKey: record.servingRegistryKey };
|
|
66
|
+
},
|
|
67
|
+
clear(): void { active = null; },
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** What the live provider registry reports as serving this frame. */
|
|
72
|
+
export interface ServingModelIdentity {
|
|
73
|
+
readonly id: string;
|
|
74
|
+
readonly provider: string;
|
|
75
|
+
readonly registryKey?: string | undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface ActiveModelInputs {
|
|
79
|
+
/** Live registry answer — who is actually serving right now. */
|
|
80
|
+
readonly serving: ServingModelIdentity;
|
|
81
|
+
/** The user's configured selection as a registry key (config `provider.model`). */
|
|
82
|
+
readonly configuredRegistryKey: string | undefined;
|
|
83
|
+
/** The configured selection's footer label (session `runtime.model`). */
|
|
84
|
+
readonly configuredLabel: string | undefined;
|
|
85
|
+
/** The configured selection's provider id (session `runtime.provider`). */
|
|
86
|
+
readonly configuredProvider: string | undefined;
|
|
87
|
+
/** The live failover record, when one is in effect. */
|
|
88
|
+
readonly failover: FailoverTurnRecord | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface ActiveModelDisplay {
|
|
92
|
+
/** Model text for the header's right-hand segment. */
|
|
93
|
+
readonly headerModel: string;
|
|
94
|
+
/** Provider text for the header's right-hand segment. */
|
|
95
|
+
readonly headerProvider: string;
|
|
96
|
+
/** Model text for the footer's context-info segment. */
|
|
97
|
+
readonly footerModel: string;
|
|
98
|
+
/** Provider text for the footer's context-info segment. */
|
|
99
|
+
readonly footerProvider: string;
|
|
100
|
+
/**
|
|
101
|
+
* Divergence marker naming the configured selection, e.g.
|
|
102
|
+
* `failover from abacusai:route-llm`. Empty string when serving is the
|
|
103
|
+
* configured selection — in that case both surfaces render exactly as they
|
|
104
|
+
* did before this resolver existed.
|
|
105
|
+
*/
|
|
106
|
+
readonly divergenceNote: string;
|
|
107
|
+
/** True when the serving backend is not the user's configured selection. */
|
|
108
|
+
readonly diverged: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Decide what the header and footer say about the active model this frame.
|
|
113
|
+
*
|
|
114
|
+
* Divergence requires BOTH a known live registry key and a known configured
|
|
115
|
+
* selection; if either is missing the resolver reports no divergence rather
|
|
116
|
+
* than guessing, because a false "failover" marker would be its own lie. The
|
|
117
|
+
* configured selection is matched against the serving registry key AND the
|
|
118
|
+
* bare serving model id, so a config value stored as a bare model id (rather
|
|
119
|
+
* than the `provider:model` registry key) does not read as permanent divergence.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveActiveModelDisplay(input: ActiveModelInputs): ActiveModelDisplay {
|
|
122
|
+
const servingKey = input.serving.registryKey;
|
|
123
|
+
const configuredKey = input.configuredRegistryKey;
|
|
124
|
+
const diverged = Boolean(
|
|
125
|
+
servingKey && configuredKey && configuredKey !== servingKey && configuredKey !== input.serving.id,
|
|
126
|
+
);
|
|
127
|
+
if (!diverged) {
|
|
128
|
+
return {
|
|
129
|
+
headerModel: input.serving.id,
|
|
130
|
+
headerProvider: input.serving.provider,
|
|
131
|
+
// Footer keeps its own established label form (runtime.model) when there
|
|
132
|
+
// is nothing to correct, so the unchanged case renders byte-identically.
|
|
133
|
+
footerModel: input.configuredLabel ?? input.serving.id,
|
|
134
|
+
footerProvider: input.configuredProvider ?? input.serving.provider,
|
|
135
|
+
divergenceNote: '',
|
|
136
|
+
diverged: false,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
// Serving != configured. Both surfaces now name the SERVING backend, plus a
|
|
140
|
+
// marker naming the configured selection it departed from. The failover
|
|
141
|
+
// record supplies the reason only when it matches the backend actually
|
|
142
|
+
// serving; any other divergence is described without claiming a cause.
|
|
143
|
+
const causedByFailover = input.failover !== null && input.failover.servingRegistryKey === servingKey;
|
|
144
|
+
const reason = causedByFailover ? 'failover from' : 'not the configured';
|
|
145
|
+
return {
|
|
146
|
+
headerModel: input.serving.id,
|
|
147
|
+
headerProvider: input.serving.provider,
|
|
148
|
+
footerModel: input.serving.id,
|
|
149
|
+
footerProvider: input.serving.provider,
|
|
150
|
+
divergenceNote: `${reason} ${configuredKey}`,
|
|
151
|
+
diverged: true,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* conversation-user-receipts.ts — tracks which system-role message indices
|
|
3
|
+
* are a direct receipt for an explicit user action (e.g. answering the
|
|
4
|
+
* startup recovery modal's Resume/Keep/Remove choice), as opposed to ambient
|
|
5
|
+
* boot chatter (provider registration, the resume notice, etc.). Kept in its
|
|
6
|
+
* own file so ConversationManager (core/conversation.ts) — already near its
|
|
7
|
+
* line-count gate — doesn't have to carry the bookkeeping inline.
|
|
8
|
+
*
|
|
9
|
+
* ConversationManager.rebuildHistory()'s splash-vs-transcript check consults
|
|
10
|
+
* this via ConversationManager.addUserActionReceipt(): a receipt the user is
|
|
11
|
+
* actively waiting on must displace the splash the way a user message does,
|
|
12
|
+
* while quiet startup plumbing keeps rendering underneath it. Indices mirror
|
|
13
|
+
* messageKindRegistry's own lifecycle — cleared on resetAll(), purged for
|
|
14
|
+
* indices an undo() frees for reuse, deleted when a later add overwrites the
|
|
15
|
+
* same recycled index with a non-receipt message.
|
|
16
|
+
*/
|
|
17
|
+
export class UserReceiptIndices {
|
|
18
|
+
private readonly indices = new Set<number>();
|
|
19
|
+
|
|
20
|
+
add(index: number): void {
|
|
21
|
+
this.indices.add(index);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
delete(index: number): void {
|
|
25
|
+
this.indices.delete(index);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
has(index: number): boolean {
|
|
29
|
+
return this.indices.has(index);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
clear(): void {
|
|
33
|
+
this.indices.clear();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Drop every tracked index >= minIndex — mirrors undo()'s tail-splice. */
|
|
37
|
+
purgeFrom(minIndex: number): void {
|
|
38
|
+
for (const key of this.indices) {
|
|
39
|
+
if (key >= minIndex) this.indices.delete(key);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/core/conversation.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
} from '@pellux/goodvibes-sdk/platform/core';
|
|
15
15
|
import type { BlockMeta } from './conversation-types.ts';
|
|
16
16
|
import { MessageLineCache } from './conversation-line-cache.ts';
|
|
17
|
+
import { UserReceiptIndices } from './conversation-user-receipts.ts';
|
|
17
18
|
import {
|
|
18
19
|
addConversationSplashScreen,
|
|
19
20
|
conversationTextToLines,
|
|
@@ -139,6 +140,8 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
139
140
|
private errorLineRegistry: number[] = [];
|
|
140
141
|
/** Maps message index → SystemMessageKind for system-role messages. */
|
|
141
142
|
private messageKindRegistry: Map<number, SystemMessageKind> = new Map();
|
|
143
|
+
/** See conversation-user-receipts.ts. */
|
|
144
|
+
private userReceiptIndices = new UserReceiptIndices();
|
|
142
145
|
/** Streaming block start line in history buffer (for incremental streaming update). */
|
|
143
146
|
private streamingStartLine = -1;
|
|
144
147
|
/**
|
|
@@ -221,25 +224,21 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
221
224
|
// stale kind (e.g. 'operational') and silently mis-classify the new message.
|
|
222
225
|
const nextIndex = this.getMessageSnapshot().length;
|
|
223
226
|
this.messageKindRegistry.delete(nextIndex);
|
|
227
|
+
this.userReceiptIndices.delete(nextIndex);
|
|
224
228
|
super.addSystemMessage(content);
|
|
225
229
|
this.markDirty();
|
|
226
230
|
}
|
|
227
231
|
|
|
228
232
|
/**
|
|
229
|
-
* addTypedSystemMessage -
|
|
230
|
-
*
|
|
231
|
-
*
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
* Called by SystemMessageRouter.routeTypedSystemMessage when routing to the
|
|
235
|
-
* conversation surface. Falls back to addSystemMessage for callers that do
|
|
236
|
-
* not have kind information.
|
|
233
|
+
* addTypedSystemMessage - System message with an explicit kind tag, stored
|
|
234
|
+
* in messageKindRegistry. `isUserReceipt` (conversation-user-receipts.ts)
|
|
235
|
+
* additionally marks it for rebuildHistory()'s splash check — only for a
|
|
236
|
+
* direct receipt to an explicit user action, never ambient boot chatter.
|
|
237
237
|
*/
|
|
238
|
-
public addTypedSystemMessage(content: string, kind: SystemMessageKind): void {
|
|
239
|
-
// getMessageSnapshot().length is the index this message will receive after
|
|
240
|
-
// addSystemMessage appends it to the messages array.
|
|
238
|
+
public addTypedSystemMessage(content: string, kind: SystemMessageKind, opts?: { isUserReceipt?: boolean }): void {
|
|
241
239
|
const nextIndex = this.getMessageSnapshot().length;
|
|
242
240
|
this.messageKindRegistry.set(nextIndex, kind);
|
|
241
|
+
if (opts?.isUserReceipt) this.userReceiptIndices.add(nextIndex); else this.userReceiptIndices.delete(nextIndex);
|
|
243
242
|
super.addSystemMessage(content);
|
|
244
243
|
this.markDirty();
|
|
245
244
|
}
|
|
@@ -256,6 +255,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
256
255
|
for (const key of this.messageKindRegistry.keys()) {
|
|
257
256
|
if (key >= postUndoCount) this.messageKindRegistry.delete(key);
|
|
258
257
|
}
|
|
258
|
+
this.userReceiptIndices.purgeFrom(postUndoCount);
|
|
259
259
|
this.markDirty();
|
|
260
260
|
}
|
|
261
261
|
return result;
|
|
@@ -357,6 +357,7 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
357
357
|
this.messageLineRegistry = [];
|
|
358
358
|
this.errorLineRegistry = [];
|
|
359
359
|
this.messageKindRegistry = new Map();
|
|
360
|
+
this.userReceiptIndices.clear();
|
|
360
361
|
this.streamingStartLine = -1;
|
|
361
362
|
this._displayFromMessageIndex = 0; // full reset — show everything on next render
|
|
362
363
|
}
|
|
@@ -478,12 +479,14 @@ export class ConversationManager extends SdkConversationManager {
|
|
|
478
479
|
const displayStart = this._displayFromMessageIndex;
|
|
479
480
|
const visibleSnapshot = displayStart > 0 ? renderSnapshot.slice(displayStart) : renderSnapshot;
|
|
480
481
|
|
|
481
|
-
// Tool messages
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
const displayMessages = visibleSnapshot.filter(
|
|
485
|
-
(m
|
|
486
|
-
|
|
482
|
+
// Tool/system messages aren't visible splash-purposes content, except a
|
|
483
|
+
// system message in userReceiptIndices. visibleSnapshot mirrors the
|
|
484
|
+
// snapshot 1:1 (only front-sliced), so index i is absolute displayStart+i.
|
|
485
|
+
const displayMessages = visibleSnapshot.filter((m, i) => {
|
|
486
|
+
if (m.role === 'tool') return false;
|
|
487
|
+
if (m.role === 'system') return this.userReceiptIndices.has(displayStart + i);
|
|
488
|
+
return true;
|
|
489
|
+
});
|
|
487
490
|
|
|
488
491
|
if (displayMessages.length === 0 && displayStart === 0 && !this.suppressSplash) {
|
|
489
492
|
this.addSplashScreen(width);
|