@oh-my-pi/pi-coding-agent 3.3.1337 → 3.5.1337
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 +32 -0
- package/package.json +4 -4
- package/src/capability/rule.ts +4 -0
- package/src/core/agent-session.ts +92 -1
- package/src/core/sdk.ts +27 -0
- package/src/core/session-manager.ts +60 -4
- package/src/core/settings-manager.ts +101 -0
- package/src/core/system-prompt.ts +15 -0
- package/src/core/title-generator.ts +28 -6
- package/src/core/tools/index.ts +6 -0
- package/src/core/tools/rulebook.ts +124 -0
- package/src/core/ttsr.ts +211 -0
- package/src/discovery/builtin.ts +1 -0
- package/src/discovery/cline.ts +2 -0
- package/src/discovery/cursor.ts +2 -0
- package/src/discovery/windsurf.ts +3 -0
- package/src/modes/interactive/components/extensions/extension-dashboard.ts +297 -0
- package/src/modes/interactive/components/extensions/extension-list.ts +477 -0
- package/src/modes/interactive/components/extensions/index.ts +9 -0
- package/src/modes/interactive/components/extensions/inspector-panel.ts +313 -0
- package/src/modes/interactive/components/extensions/state-manager.ts +558 -0
- package/src/modes/interactive/components/extensions/types.ts +191 -0
- package/src/modes/interactive/components/footer.ts +15 -1
- package/src/modes/interactive/components/settings-defs.ts +31 -31
- package/src/modes/interactive/components/settings-selector.ts +0 -1
- package/src/modes/interactive/components/ttsr-notification.ts +82 -0
- package/src/modes/interactive/interactive-mode.ts +54 -314
- package/src/modes/print-mode.ts +34 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [3.5.1337] - 2026-01-03
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Added session header and footer output in text mode showing version, model, provider, thinking level, and session ID
|
|
10
|
+
- Added Extension Control Center dashboard accessible via `/extensions` command for unified management of all providers and extensions
|
|
11
|
+
- Added ability to enable/disable individual extensions with persistent settings
|
|
12
|
+
- Added three-column dashboard layout with sidebar tree, extension list, and inspector panel
|
|
13
|
+
- Added fuzzy search filtering for extensions in the dashboard
|
|
14
|
+
- Added keyboard navigation with Tab to cycle panes, j/k for navigation, Space to toggle, Enter to expand/collapse
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Redesigned Extension Control Center from 3-column layout to tabbed interface with horizontal provider tabs and 2-column grid
|
|
19
|
+
- Replaced sidebar tree navigation with provider tabs using TAB/Shift+TAB cycling
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Fixed title generation flag not resetting when starting a new session
|
|
24
|
+
|
|
25
|
+
## [3.4.1337] - 2026-01-03
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- Added Time Traveling Stream Rules (TTSR) feature that monitors agent output for pattern matches and injects rule reminders mid-stream
|
|
30
|
+
- Added `ttsr_trigger` frontmatter field for rules to define regex patterns that trigger mid-stream injection
|
|
31
|
+
- Added TTSR settings for enabled state, context mode (keep/discard partial output), and repeat mode (once/after-gap)
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- Fixed excessive subprocess spawns by caching git status for 1 second in the footer component
|
|
36
|
+
|
|
5
37
|
## [3.3.1337] - 2026-01-03
|
|
6
38
|
|
|
7
39
|
### Changed
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.1337",
|
|
4
4
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"ompConfig": {
|
|
@@ -39,9 +39,9 @@
|
|
|
39
39
|
"prepublishOnly": "bun run generate-template && bun run clean && bun run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@oh-my-pi/pi-agent-core": "3.
|
|
43
|
-
"@oh-my-pi/pi-ai": "3.
|
|
44
|
-
"@oh-my-pi/pi-tui": "3.
|
|
42
|
+
"@oh-my-pi/pi-agent-core": "3.5.1337",
|
|
43
|
+
"@oh-my-pi/pi-ai": "3.5.1337",
|
|
44
|
+
"@oh-my-pi/pi-tui": "3.5.1337",
|
|
45
45
|
"@sinclair/typebox": "^0.34.46",
|
|
46
46
|
"ajv": "^8.17.1",
|
|
47
47
|
"chalk": "^5.5.0",
|
package/src/capability/rule.ts
CHANGED
|
@@ -15,6 +15,8 @@ export interface RuleFrontmatter {
|
|
|
15
15
|
description?: string;
|
|
16
16
|
globs?: string[];
|
|
17
17
|
alwaysApply?: boolean;
|
|
18
|
+
/** Regex pattern that triggers time-traveling rule injection */
|
|
19
|
+
ttsr_trigger?: string;
|
|
18
20
|
[key: string]: unknown;
|
|
19
21
|
}
|
|
20
22
|
|
|
@@ -34,6 +36,8 @@ export interface Rule {
|
|
|
34
36
|
alwaysApply?: boolean;
|
|
35
37
|
/** Description (for agent-requested rules) */
|
|
36
38
|
description?: string;
|
|
39
|
+
/** Regex pattern that triggers time-traveling rule injection */
|
|
40
|
+
ttsrTrigger?: string;
|
|
37
41
|
/** Source metadata */
|
|
38
42
|
_source: SourceMeta;
|
|
39
43
|
}
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type { Agent, AgentEvent, AgentMessage, AgentState, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
17
17
|
import type { AssistantMessage, ImageContent, Message, Model, TextContent } from "@oh-my-pi/pi-ai";
|
|
18
18
|
import { isContextOverflow, modelsAreEqual, supportsXhigh } from "@oh-my-pi/pi-ai";
|
|
19
|
+
import type { Rule } from "../capability/rule";
|
|
19
20
|
import { getAuthPath } from "../config";
|
|
20
21
|
import { type BashResult, executeBash as executeBashCommand } from "./bash-executor";
|
|
21
22
|
import {
|
|
@@ -47,6 +48,7 @@ import type { ModelRegistry } from "./model-registry";
|
|
|
47
48
|
import type { BranchSummaryEntry, CompactionEntry, NewSessionOptions, SessionManager } from "./session-manager";
|
|
48
49
|
import type { SettingsManager, SkillsSettings } from "./settings-manager";
|
|
49
50
|
import { expandSlashCommand, type FileSlashCommand, parseCommandArgs } from "./slash-commands";
|
|
51
|
+
import type { TtsrManager } from "./ttsr";
|
|
50
52
|
|
|
51
53
|
/** Session-specific events that extend the core AgentEvent */
|
|
52
54
|
export type AgentSessionEvent =
|
|
@@ -54,7 +56,8 @@ export type AgentSessionEvent =
|
|
|
54
56
|
| { type: "auto_compaction_start"; reason: "threshold" | "overflow" }
|
|
55
57
|
| { type: "auto_compaction_end"; result: CompactionResult | undefined; aborted: boolean; willRetry: boolean }
|
|
56
58
|
| { type: "auto_retry_start"; attempt: number; maxAttempts: number; delayMs: number; errorMessage: string }
|
|
57
|
-
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
59
|
+
| { type: "auto_retry_end"; success: boolean; attempt: number; finalError?: string }
|
|
60
|
+
| { type: "ttsr_triggered"; rules: Rule[] };
|
|
58
61
|
|
|
59
62
|
/** Listener function for agent session events */
|
|
60
63
|
export type AgentSessionEventListener = (event: AgentSessionEvent) => void;
|
|
@@ -80,6 +83,8 @@ export interface AgentSessionConfig {
|
|
|
80
83
|
skillsSettings?: Required<SkillsSettings>;
|
|
81
84
|
/** Model registry for API key resolution and model discovery */
|
|
82
85
|
modelRegistry: ModelRegistry;
|
|
86
|
+
/** TTSR manager for time-traveling stream rules */
|
|
87
|
+
ttsrManager?: TtsrManager;
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/** Options for AgentSession.prompt() */
|
|
@@ -179,6 +184,11 @@ export class AgentSession {
|
|
|
179
184
|
// Model registry for API key resolution
|
|
180
185
|
private _modelRegistry: ModelRegistry;
|
|
181
186
|
|
|
187
|
+
// TTSR manager for time-traveling stream rules
|
|
188
|
+
private _ttsrManager: TtsrManager | undefined = undefined;
|
|
189
|
+
private _pendingTtsrInjections: Rule[] = [];
|
|
190
|
+
private _ttsrAbortPending = false;
|
|
191
|
+
|
|
182
192
|
constructor(config: AgentSessionConfig) {
|
|
183
193
|
this.agent = config.agent;
|
|
184
194
|
this.sessionManager = config.sessionManager;
|
|
@@ -190,6 +200,7 @@ export class AgentSession {
|
|
|
190
200
|
this._customCommands = config.customCommands ?? [];
|
|
191
201
|
this._skillsSettings = config.skillsSettings;
|
|
192
202
|
this._modelRegistry = config.modelRegistry;
|
|
203
|
+
this._ttsrManager = config.ttsrManager;
|
|
193
204
|
|
|
194
205
|
// Always subscribe to agent events for internal handling
|
|
195
206
|
// (session persistence, hooks, auto-compaction, retry logic)
|
|
@@ -201,6 +212,16 @@ export class AgentSession {
|
|
|
201
212
|
return this._modelRegistry;
|
|
202
213
|
}
|
|
203
214
|
|
|
215
|
+
/** TTSR manager for time-traveling stream rules */
|
|
216
|
+
get ttsrManager(): TtsrManager | undefined {
|
|
217
|
+
return this._ttsrManager;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Whether a TTSR abort is pending (stream was aborted to inject rules) */
|
|
221
|
+
get isTtsrAbortPending(): boolean {
|
|
222
|
+
return this._ttsrAbortPending;
|
|
223
|
+
}
|
|
224
|
+
|
|
204
225
|
// =========================================================================
|
|
205
226
|
// Event Subscription
|
|
206
227
|
// =========================================================================
|
|
@@ -239,6 +260,60 @@ export class AgentSession {
|
|
|
239
260
|
// Notify all listeners
|
|
240
261
|
this._emit(event);
|
|
241
262
|
|
|
263
|
+
// TTSR: Reset buffer on turn start
|
|
264
|
+
if (event.type === "turn_start" && this._ttsrManager) {
|
|
265
|
+
this._ttsrManager.resetBuffer();
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// TTSR: Increment message count on turn end (for repeat-after-gap tracking)
|
|
269
|
+
if (event.type === "turn_end" && this._ttsrManager) {
|
|
270
|
+
this._ttsrManager.incrementMessageCount();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// TTSR: Check for pattern matches on text deltas and tool call argument deltas
|
|
274
|
+
if (event.type === "message_update" && this._ttsrManager?.hasRules()) {
|
|
275
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
276
|
+
// Monitor both assistant prose (text_delta) and tool call arguments (toolcall_delta)
|
|
277
|
+
if (assistantEvent.type === "text_delta" || assistantEvent.type === "toolcall_delta") {
|
|
278
|
+
this._ttsrManager.appendToBuffer(assistantEvent.delta);
|
|
279
|
+
const matches = this._ttsrManager.check(this._ttsrManager.getBuffer());
|
|
280
|
+
if (matches.length > 0) {
|
|
281
|
+
// Mark rules as injected so they don't trigger again
|
|
282
|
+
this._ttsrManager.markInjected(matches);
|
|
283
|
+
// Store for injection on retry
|
|
284
|
+
this._pendingTtsrInjections.push(...matches);
|
|
285
|
+
// Emit TTSR event before aborting (so UI can handle it)
|
|
286
|
+
this._ttsrAbortPending = true;
|
|
287
|
+
this._emit({ type: "ttsr_triggered", rules: matches });
|
|
288
|
+
// Abort the stream
|
|
289
|
+
this.agent.abort();
|
|
290
|
+
// Schedule retry after a short delay
|
|
291
|
+
setTimeout(async () => {
|
|
292
|
+
this._ttsrAbortPending = false;
|
|
293
|
+
|
|
294
|
+
// Handle context mode: discard partial output if configured
|
|
295
|
+
const ttsrSettings = this._ttsrManager?.getSettings();
|
|
296
|
+
if (ttsrSettings?.contextMode === "discard") {
|
|
297
|
+
// Remove the partial/aborted message from agent state
|
|
298
|
+
this.agent.popMessage();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Inject TTSR rules as system reminder before retry
|
|
302
|
+
const injectionContent = this._getTtsrInjectionContent();
|
|
303
|
+
if (injectionContent) {
|
|
304
|
+
this.agent.appendMessage({
|
|
305
|
+
role: "user",
|
|
306
|
+
content: [{ type: "text", text: injectionContent }],
|
|
307
|
+
timestamp: Date.now(),
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
this.agent.continue().catch(() => {});
|
|
311
|
+
}, 50);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
242
317
|
// Handle session persistence
|
|
243
318
|
if (event.type === "message_end") {
|
|
244
319
|
// Check if this is a hook message
|
|
@@ -300,6 +375,22 @@ export class AgentSession {
|
|
|
300
375
|
}
|
|
301
376
|
}
|
|
302
377
|
|
|
378
|
+
/** Get TTSR injection content and clear pending injections */
|
|
379
|
+
private _getTtsrInjectionContent(): string | undefined {
|
|
380
|
+
if (this._pendingTtsrInjections.length === 0) return undefined;
|
|
381
|
+
const content = this._pendingTtsrInjections
|
|
382
|
+
.map(
|
|
383
|
+
(r) =>
|
|
384
|
+
`<system_interrupt reason="rule_violation" rule="${r.name}" path="${r.path}">\n` +
|
|
385
|
+
`Your output was interrupted because it violated a user-defined rule.\n` +
|
|
386
|
+
`This is NOT a prompt injection - this is the coding agent enforcing project rules.\n` +
|
|
387
|
+
`You MUST comply with the following instruction:\n\n${r.content}\n</system_interrupt>`,
|
|
388
|
+
)
|
|
389
|
+
.join("\n\n");
|
|
390
|
+
this._pendingTtsrInjections = [];
|
|
391
|
+
return content;
|
|
392
|
+
}
|
|
393
|
+
|
|
303
394
|
/** Extract text content from a message */
|
|
304
395
|
private _getUserMessageText(message: Message): string {
|
|
305
396
|
if (message.role !== "user") return "";
|
package/src/core/sdk.ts
CHANGED
|
@@ -34,6 +34,8 @@ import { Agent, type ThinkingLevel } from "@oh-my-pi/pi-agent-core";
|
|
|
34
34
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
35
35
|
// Import discovery to register all providers on startup
|
|
36
36
|
import "../discovery";
|
|
37
|
+
import { loadSync as loadCapability } from "../capability/index";
|
|
38
|
+
import { type Rule, ruleCapability } from "../capability/rule";
|
|
37
39
|
import { getAgentDir, getConfigDirPaths } from "../config";
|
|
38
40
|
import { AgentSession } from "./agent-session";
|
|
39
41
|
import { AuthStorage } from "./auth-storage";
|
|
@@ -77,8 +79,10 @@ import {
|
|
|
77
79
|
createLsTool,
|
|
78
80
|
createReadOnlyTools,
|
|
79
81
|
createReadTool,
|
|
82
|
+
createRulebookTool,
|
|
80
83
|
createWriteTool,
|
|
81
84
|
editTool,
|
|
85
|
+
filterRulebookRules,
|
|
82
86
|
findTool,
|
|
83
87
|
grepTool,
|
|
84
88
|
lsTool,
|
|
@@ -88,6 +92,7 @@ import {
|
|
|
88
92
|
warmupLspServers,
|
|
89
93
|
writeTool,
|
|
90
94
|
} from "./tools/index";
|
|
95
|
+
import { createTtsrManager } from "./ttsr";
|
|
91
96
|
|
|
92
97
|
// Types
|
|
93
98
|
|
|
@@ -601,6 +606,20 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
601
606
|
const skills = options.skills ?? discoverSkills(cwd, agentDir, settingsManager.getSkillsSettings());
|
|
602
607
|
time("discoverSkills");
|
|
603
608
|
|
|
609
|
+
// Discover rules
|
|
610
|
+
const ttsrManager = createTtsrManager(settingsManager.getTtsrSettings());
|
|
611
|
+
const rulesResult = loadCapability<Rule>(ruleCapability.id, { cwd });
|
|
612
|
+
for (const rule of rulesResult.items) {
|
|
613
|
+
if (rule.ttsrTrigger) {
|
|
614
|
+
ttsrManager.addRule(rule);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
time("discoverTtsrRules");
|
|
618
|
+
|
|
619
|
+
// Filter rules for the rulebook (non-TTSR, non-alwaysApply, with descriptions)
|
|
620
|
+
const rulebookRules = filterRulebookRules(rulesResult.items);
|
|
621
|
+
time("filterRulebookRules");
|
|
622
|
+
|
|
604
623
|
const contextFiles = options.contextFiles ?? discoverContextFiles(cwd, agentDir);
|
|
605
624
|
time("discoverContextFiles");
|
|
606
625
|
|
|
@@ -744,6 +763,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
744
763
|
|
|
745
764
|
let allToolsArray: Tool[] = [...builtInTools, ...wrappedCustomTools];
|
|
746
765
|
|
|
766
|
+
// Add rulebook tool if there are rules with descriptions (always enabled, regardless of --tools)
|
|
767
|
+
if (rulebookRules.length > 0) {
|
|
768
|
+
allToolsArray.push(createRulebookTool(rulebookRules));
|
|
769
|
+
}
|
|
770
|
+
|
|
747
771
|
// Filter out hidden tools unless explicitly requested
|
|
748
772
|
if (options.explicitTools) {
|
|
749
773
|
const explicitSet = new Set(options.explicitTools);
|
|
@@ -768,6 +792,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
768
792
|
cwd,
|
|
769
793
|
skills,
|
|
770
794
|
contextFiles,
|
|
795
|
+
rulebookRules,
|
|
771
796
|
});
|
|
772
797
|
time("buildSystemPrompt");
|
|
773
798
|
|
|
@@ -778,6 +803,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
778
803
|
cwd,
|
|
779
804
|
skills,
|
|
780
805
|
contextFiles,
|
|
806
|
+
rulebookRules,
|
|
781
807
|
customPrompt: options.systemPrompt,
|
|
782
808
|
});
|
|
783
809
|
} else {
|
|
@@ -847,6 +873,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
847
873
|
customCommands: customCommandsResult.commands,
|
|
848
874
|
skillsSettings: settingsManager.getSkillsSettings(),
|
|
849
875
|
modelRegistry,
|
|
876
|
+
ttsrManager,
|
|
850
877
|
});
|
|
851
878
|
time("createAgentSession");
|
|
852
879
|
|
|
@@ -107,6 +107,13 @@ export interface LabelEntry extends SessionEntryBase {
|
|
|
107
107
|
label: string | undefined;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** TTSR injection entry - tracks which time-traveling rules have been injected this session. */
|
|
111
|
+
export interface TtsrInjectionEntry extends SessionEntryBase {
|
|
112
|
+
type: "ttsr_injection";
|
|
113
|
+
/** Names of rules that were injected */
|
|
114
|
+
injectedRules: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
110
117
|
/**
|
|
111
118
|
* Custom message entry for hooks to inject messages into LLM context.
|
|
112
119
|
* Use customType to identify your hook's entries.
|
|
@@ -136,7 +143,8 @@ export type SessionEntry =
|
|
|
136
143
|
| BranchSummaryEntry
|
|
137
144
|
| CustomEntry
|
|
138
145
|
| CustomMessageEntry
|
|
139
|
-
| LabelEntry
|
|
146
|
+
| LabelEntry
|
|
147
|
+
| TtsrInjectionEntry;
|
|
140
148
|
|
|
141
149
|
/** Raw file entry (includes header) */
|
|
142
150
|
export type FileEntry = SessionHeader | SessionEntry;
|
|
@@ -154,6 +162,8 @@ export interface SessionContext {
|
|
|
154
162
|
thinkingLevel: string;
|
|
155
163
|
/** Model roles: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
156
164
|
models: Record<string, string>;
|
|
165
|
+
/** Names of TTSR rules that have been injected this session */
|
|
166
|
+
injectedTtsrRules: string[];
|
|
157
167
|
}
|
|
158
168
|
|
|
159
169
|
export interface SessionInfo {
|
|
@@ -295,7 +305,7 @@ export function buildSessionContext(
|
|
|
295
305
|
let leaf: SessionEntry | undefined;
|
|
296
306
|
if (leafId === null) {
|
|
297
307
|
// Explicitly null - return no messages (navigated to before first entry)
|
|
298
|
-
return { messages: [], thinkingLevel: "off", models: {} };
|
|
308
|
+
return { messages: [], thinkingLevel: "off", models: {}, injectedTtsrRules: [] };
|
|
299
309
|
}
|
|
300
310
|
if (leafId) {
|
|
301
311
|
leaf = byId.get(leafId);
|
|
@@ -306,7 +316,7 @@ export function buildSessionContext(
|
|
|
306
316
|
}
|
|
307
317
|
|
|
308
318
|
if (!leaf) {
|
|
309
|
-
return { messages: [], thinkingLevel: "off", models: {} };
|
|
319
|
+
return { messages: [], thinkingLevel: "off", models: {}, injectedTtsrRules: [] };
|
|
310
320
|
}
|
|
311
321
|
|
|
312
322
|
// Walk from leaf to root, collecting path
|
|
@@ -321,6 +331,7 @@ export function buildSessionContext(
|
|
|
321
331
|
let thinkingLevel = "off";
|
|
322
332
|
const models: Record<string, string> = {};
|
|
323
333
|
let compaction: CompactionEntry | null = null;
|
|
334
|
+
const injectedTtsrRulesSet = new Set<string>();
|
|
324
335
|
|
|
325
336
|
for (const entry of path) {
|
|
326
337
|
if (entry.type === "thinking_level_change") {
|
|
@@ -336,9 +347,16 @@ export function buildSessionContext(
|
|
|
336
347
|
models.default = `${entry.message.provider}/${entry.message.model}`;
|
|
337
348
|
} else if (entry.type === "compaction") {
|
|
338
349
|
compaction = entry;
|
|
350
|
+
} else if (entry.type === "ttsr_injection") {
|
|
351
|
+
// Collect injected TTSR rule names
|
|
352
|
+
for (const ruleName of entry.injectedRules) {
|
|
353
|
+
injectedTtsrRulesSet.add(ruleName);
|
|
354
|
+
}
|
|
339
355
|
}
|
|
340
356
|
}
|
|
341
357
|
|
|
358
|
+
const injectedTtsrRules = Array.from(injectedTtsrRulesSet);
|
|
359
|
+
|
|
342
360
|
// Build messages and collect corresponding entries
|
|
343
361
|
// When there's a compaction, we need to:
|
|
344
362
|
// 1. Emit summary first (entry = compaction)
|
|
@@ -389,7 +407,7 @@ export function buildSessionContext(
|
|
|
389
407
|
}
|
|
390
408
|
}
|
|
391
409
|
|
|
392
|
-
return { messages, thinkingLevel, models };
|
|
410
|
+
return { messages, thinkingLevel, models, injectedTtsrRules };
|
|
393
411
|
}
|
|
394
412
|
|
|
395
413
|
/**
|
|
@@ -814,6 +832,44 @@ export class SessionManager {
|
|
|
814
832
|
return entry.id;
|
|
815
833
|
}
|
|
816
834
|
|
|
835
|
+
// =========================================================================
|
|
836
|
+
// TTSR (Time Traveling Stream Rules)
|
|
837
|
+
// =========================================================================
|
|
838
|
+
|
|
839
|
+
/**
|
|
840
|
+
* Append a TTSR injection entry recording which rules were injected.
|
|
841
|
+
* @param ruleNames Names of rules that were injected
|
|
842
|
+
* @returns Entry id
|
|
843
|
+
*/
|
|
844
|
+
appendTtsrInjection(ruleNames: string[]): string {
|
|
845
|
+
const entry: TtsrInjectionEntry = {
|
|
846
|
+
type: "ttsr_injection",
|
|
847
|
+
id: generateId(this.byId),
|
|
848
|
+
parentId: this.leafId,
|
|
849
|
+
timestamp: new Date().toISOString(),
|
|
850
|
+
injectedRules: ruleNames,
|
|
851
|
+
};
|
|
852
|
+
this._appendEntry(entry);
|
|
853
|
+
return entry.id;
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
/**
|
|
857
|
+
* Get all unique TTSR rule names that have been injected in the current branch.
|
|
858
|
+
* Scans from root to current leaf for ttsr_injection entries.
|
|
859
|
+
*/
|
|
860
|
+
getInjectedTtsrRules(): string[] {
|
|
861
|
+
const path = this.getBranch();
|
|
862
|
+
const ruleNames = new Set<string>();
|
|
863
|
+
for (const entry of path) {
|
|
864
|
+
if (entry.type === "ttsr_injection") {
|
|
865
|
+
for (const name of entry.injectedRules) {
|
|
866
|
+
ruleNames.add(name);
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return Array.from(ruleNames);
|
|
871
|
+
}
|
|
872
|
+
|
|
817
873
|
// =========================================================================
|
|
818
874
|
// Tree Traversal
|
|
819
875
|
// =========================================================================
|
|
@@ -68,6 +68,16 @@ export interface EditSettings {
|
|
|
68
68
|
fuzzyMatch?: boolean; // default: true (accept high-confidence fuzzy matches for whitespace/indentation)
|
|
69
69
|
}
|
|
70
70
|
|
|
71
|
+
export interface TtsrSettings {
|
|
72
|
+
enabled?: boolean; // default: true
|
|
73
|
+
/** What to do with partial output when TTSR triggers: "keep" shows interrupted attempt, "discard" removes it */
|
|
74
|
+
contextMode?: "keep" | "discard"; // default: "discard"
|
|
75
|
+
/** How TTSR rules repeat: "once" = only trigger once per session, "after-gap" = can repeat after N messages */
|
|
76
|
+
repeatMode?: "once" | "after-gap"; // default: "once"
|
|
77
|
+
/** Number of messages before a rule can trigger again (only used when repeatMode is "after-gap") */
|
|
78
|
+
repeatGap?: number; // default: 10
|
|
79
|
+
}
|
|
80
|
+
|
|
71
81
|
export interface Settings {
|
|
72
82
|
lastChangelogVersion?: string;
|
|
73
83
|
/** Model roles map: { default: "provider/modelId", small: "provider/modelId", ... } */
|
|
@@ -93,7 +103,9 @@ export interface Settings {
|
|
|
93
103
|
mcp?: MCPSettings;
|
|
94
104
|
lsp?: LspSettings;
|
|
95
105
|
edit?: EditSettings;
|
|
106
|
+
ttsr?: TtsrSettings;
|
|
96
107
|
disabledProviders?: string[]; // Discovery provider IDs that are disabled
|
|
108
|
+
disabledExtensions?: string[]; // Individual extension IDs that are disabled (e.g., "skill:commit")
|
|
97
109
|
}
|
|
98
110
|
|
|
99
111
|
/** Deep merge settings: project/overrides take precedence, nested objects merge recursively */
|
|
@@ -582,4 +594,93 @@ export class SettingsManager {
|
|
|
582
594
|
this.globalSettings.disabledProviders = providerIds;
|
|
583
595
|
this.save();
|
|
584
596
|
}
|
|
597
|
+
|
|
598
|
+
getDisabledExtensions(): string[] {
|
|
599
|
+
return [...(this.settings.disabledExtensions ?? [])];
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
setDisabledExtensions(extensionIds: string[]): void {
|
|
603
|
+
this.globalSettings.disabledExtensions = extensionIds;
|
|
604
|
+
this.save();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
isExtensionEnabled(extensionId: string): boolean {
|
|
608
|
+
return !(this.settings.disabledExtensions ?? []).includes(extensionId);
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
enableExtension(extensionId: string): void {
|
|
612
|
+
const disabled = this.globalSettings.disabledExtensions ?? [];
|
|
613
|
+
const index = disabled.indexOf(extensionId);
|
|
614
|
+
if (index !== -1) {
|
|
615
|
+
disabled.splice(index, 1);
|
|
616
|
+
this.globalSettings.disabledExtensions = disabled;
|
|
617
|
+
this.save();
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
disableExtension(extensionId: string): void {
|
|
622
|
+
const disabled = this.globalSettings.disabledExtensions ?? [];
|
|
623
|
+
if (!disabled.includes(extensionId)) {
|
|
624
|
+
disabled.push(extensionId);
|
|
625
|
+
this.globalSettings.disabledExtensions = disabled;
|
|
626
|
+
this.save();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
getTtsrSettings(): TtsrSettings {
|
|
631
|
+
return this.settings.ttsr ?? {};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
setTtsrSettings(settings: TtsrSettings): void {
|
|
635
|
+
this.globalSettings.ttsr = { ...this.globalSettings.ttsr, ...settings };
|
|
636
|
+
this.save();
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
getTtsrEnabled(): boolean {
|
|
640
|
+
return this.settings.ttsr?.enabled ?? true;
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
setTtsrEnabled(enabled: boolean): void {
|
|
644
|
+
if (!this.globalSettings.ttsr) {
|
|
645
|
+
this.globalSettings.ttsr = {};
|
|
646
|
+
}
|
|
647
|
+
this.globalSettings.ttsr.enabled = enabled;
|
|
648
|
+
this.save();
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
getTtsrContextMode(): "keep" | "discard" {
|
|
652
|
+
return this.settings.ttsr?.contextMode ?? "discard";
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
setTtsrContextMode(mode: "keep" | "discard"): void {
|
|
656
|
+
if (!this.globalSettings.ttsr) {
|
|
657
|
+
this.globalSettings.ttsr = {};
|
|
658
|
+
}
|
|
659
|
+
this.globalSettings.ttsr.contextMode = mode;
|
|
660
|
+
this.save();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
getTtsrRepeatMode(): "once" | "after-gap" {
|
|
664
|
+
return this.settings.ttsr?.repeatMode ?? "once";
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
setTtsrRepeatMode(mode: "once" | "after-gap"): void {
|
|
668
|
+
if (!this.globalSettings.ttsr) {
|
|
669
|
+
this.globalSettings.ttsr = {};
|
|
670
|
+
}
|
|
671
|
+
this.globalSettings.ttsr.repeatMode = mode;
|
|
672
|
+
this.save();
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
getTtsrRepeatGap(): number {
|
|
676
|
+
return this.settings.ttsr?.repeatGap ?? 10;
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
setTtsrRepeatGap(gap: number): void {
|
|
680
|
+
if (!this.globalSettings.ttsr) {
|
|
681
|
+
this.globalSettings.ttsr = {};
|
|
682
|
+
}
|
|
683
|
+
this.globalSettings.ttsr.repeatGap = gap;
|
|
684
|
+
this.save();
|
|
685
|
+
}
|
|
585
686
|
}
|
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
import { existsSync, readFileSync } from "node:fs";
|
|
6
6
|
import chalk from "chalk";
|
|
7
7
|
import { contextFileCapability } from "../capability/context-file";
|
|
8
|
+
import type { Rule } from "../capability/rule";
|
|
8
9
|
import { systemPromptCapability } from "../capability/system-prompt";
|
|
9
10
|
import { getDocsPath, getExamplesPath, getReadmePath } from "../config";
|
|
10
11
|
import { type ContextFile, loadSync, type SystemPrompt as SystemPromptFile } from "../discovery/index";
|
|
11
12
|
import type { SkillsSettings } from "./settings-manager";
|
|
12
13
|
import { formatSkillsForPrompt, loadSkills, type Skill } from "./skills";
|
|
13
14
|
import type { ToolName } from "./tools/index";
|
|
15
|
+
import { formatRulesForPrompt } from "./tools/rulebook";
|
|
14
16
|
|
|
15
17
|
/**
|
|
16
18
|
* Execute a git command synchronously and return stdout or null on failure.
|
|
@@ -238,6 +240,8 @@ export interface BuildSystemPromptOptions {
|
|
|
238
240
|
contextFiles?: Array<{ path: string; content: string; depth?: number }>;
|
|
239
241
|
/** Pre-loaded skills (skips discovery if provided). */
|
|
240
242
|
skills?: Skill[];
|
|
243
|
+
/** Pre-loaded rulebook rules (rules with descriptions, excluding TTSR and always-apply). */
|
|
244
|
+
rulebookRules?: Rule[];
|
|
241
245
|
}
|
|
242
246
|
|
|
243
247
|
/** Build the system prompt with tools, guidelines, and context */
|
|
@@ -250,6 +254,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
250
254
|
cwd,
|
|
251
255
|
contextFiles: providedContextFiles,
|
|
252
256
|
skills: providedSkills,
|
|
257
|
+
rulebookRules,
|
|
253
258
|
} = options;
|
|
254
259
|
const resolvedCwd = cwd ?? process.cwd();
|
|
255
260
|
const resolvedCustomPrompt = resolvePromptInput(customPrompt, "system prompt");
|
|
@@ -310,6 +315,11 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
310
315
|
prompt += formatSkillsForPrompt(skills);
|
|
311
316
|
}
|
|
312
317
|
|
|
318
|
+
// Append rules section (always enabled when rules exist)
|
|
319
|
+
if (rulebookRules && rulebookRules.length > 0) {
|
|
320
|
+
prompt += formatRulesForPrompt(rulebookRules);
|
|
321
|
+
}
|
|
322
|
+
|
|
313
323
|
// Add date/time and working directory last
|
|
314
324
|
prompt += `\nCurrent date and time: ${dateTime}`;
|
|
315
325
|
prompt += `\nCurrent working directory: ${resolvedCwd}`;
|
|
@@ -419,6 +429,11 @@ Documentation:
|
|
|
419
429
|
prompt += formatSkillsForPrompt(skills);
|
|
420
430
|
}
|
|
421
431
|
|
|
432
|
+
// Append rules section (always enabled when rules exist)
|
|
433
|
+
if (rulebookRules && rulebookRules.length > 0) {
|
|
434
|
+
prompt += formatRulesForPrompt(rulebookRules);
|
|
435
|
+
}
|
|
436
|
+
|
|
422
437
|
// Add date/time and working directory last
|
|
423
438
|
prompt += `\nCurrent date and time: ${dateTime}`;
|
|
424
439
|
prompt += `\nCurrent working directory: ${resolvedCwd}`;
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { Model } from "@oh-my-pi/pi-ai";
|
|
6
6
|
import { completeSimple } from "@oh-my-pi/pi-ai";
|
|
7
|
+
import { logger } from "./logger";
|
|
7
8
|
import type { ModelRegistry } from "./model-registry";
|
|
8
9
|
import { findSmolModel } from "./model-resolver";
|
|
9
10
|
|
|
@@ -43,21 +44,35 @@ export async function generateSessionTitle(
|
|
|
43
44
|
savedSmolModel?: string,
|
|
44
45
|
): Promise<string | null> {
|
|
45
46
|
const model = await findTitleModel(registry, savedSmolModel);
|
|
46
|
-
if (!model)
|
|
47
|
+
if (!model) {
|
|
48
|
+
logger.debug("title-generator: no smol model found");
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
47
51
|
|
|
48
52
|
const apiKey = await registry.getApiKey(model);
|
|
49
|
-
if (!apiKey)
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
logger.debug("title-generator: no API key for model", { provider: model.provider, id: model.id });
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
50
57
|
|
|
51
58
|
// Truncate message if too long
|
|
52
59
|
const truncatedMessage =
|
|
53
60
|
firstMessage.length > MAX_INPUT_CHARS ? `${firstMessage.slice(0, MAX_INPUT_CHARS)}...` : firstMessage;
|
|
54
61
|
|
|
62
|
+
const request = {
|
|
63
|
+
model: `${model.provider}/${model.id}`,
|
|
64
|
+
systemPrompt: TITLE_SYSTEM_PROMPT,
|
|
65
|
+
userMessage: `<user-message>\n${truncatedMessage}\n</user-message>`,
|
|
66
|
+
maxTokens: 30,
|
|
67
|
+
};
|
|
68
|
+
logger.debug("title-generator: request", request);
|
|
69
|
+
|
|
55
70
|
try {
|
|
56
71
|
const response = await completeSimple(
|
|
57
72
|
model,
|
|
58
73
|
{
|
|
59
|
-
systemPrompt:
|
|
60
|
-
messages: [{ role: "user", content:
|
|
74
|
+
systemPrompt: request.systemPrompt,
|
|
75
|
+
messages: [{ role: "user", content: request.userMessage, timestamp: Date.now() }],
|
|
61
76
|
},
|
|
62
77
|
{
|
|
63
78
|
apiKey,
|
|
@@ -74,13 +89,20 @@ export async function generateSessionTitle(
|
|
|
74
89
|
}
|
|
75
90
|
title = title.trim();
|
|
76
91
|
|
|
77
|
-
|
|
92
|
+
logger.debug("title-generator: response", {
|
|
93
|
+
title,
|
|
94
|
+
usage: response.usage,
|
|
95
|
+
stopReason: response.stopReason,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (!title) {
|
|
78
99
|
return null;
|
|
79
100
|
}
|
|
80
101
|
|
|
81
102
|
// Clean up: remove quotes, trailing punctuation
|
|
82
103
|
return title.replace(/^["']|["']$/g, "").replace(/[.!?]$/, "");
|
|
83
|
-
} catch {
|
|
104
|
+
} catch (err) {
|
|
105
|
+
logger.debug("title-generator: error", { error: err instanceof Error ? err.message : String(err) });
|
|
84
106
|
return null;
|
|
85
107
|
}
|
|
86
108
|
}
|