opencode-miniterm 1.0.0
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/.prettierrc +16 -0
- package/AGENTS.md +243 -0
- package/README.md +5 -0
- package/bun.lock +108 -0
- package/package.json +34 -0
- package/src/ansi.ts +22 -0
- package/src/commands/agents.ts +178 -0
- package/src/commands/debug.ts +74 -0
- package/src/commands/details.ts +17 -0
- package/src/commands/diff.ts +155 -0
- package/src/commands/exit.ts +17 -0
- package/src/commands/init.ts +35 -0
- package/src/commands/kill.ts +33 -0
- package/src/commands/log.ts +24 -0
- package/src/commands/models.ts +218 -0
- package/src/commands/new.ts +42 -0
- package/src/commands/page.ts +78 -0
- package/src/commands/quit.ts +17 -0
- package/src/commands/run.ts +34 -0
- package/src/commands/sessions.ts +257 -0
- package/src/commands/undo.ts +65 -0
- package/src/config.ts +56 -0
- package/src/index.ts +990 -0
- package/src/render.ts +320 -0
- package/src/types.ts +11 -0
- package/test/render.test.ts +390 -0
- package/test/test.ts +115 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import { type State, getLogDir } from "../index";
|
|
5
|
+
import type { Command } from "../types";
|
|
6
|
+
|
|
7
|
+
let command: Command = {
|
|
8
|
+
name: "/debug",
|
|
9
|
+
description: "Save raw events from the last request to a JSON file",
|
|
10
|
+
run,
|
|
11
|
+
running: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default command;
|
|
15
|
+
|
|
16
|
+
function run(_client: OpencodeClient, state: State): void {
|
|
17
|
+
if (state.allEvents.length === 0) {
|
|
18
|
+
console.log("No parts stored yet. Send a message first.");
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Create a copy of events to modify
|
|
23
|
+
const eventsCopy = JSON.parse(JSON.stringify(state.allEvents));
|
|
24
|
+
|
|
25
|
+
for (let part of eventsCopy) {
|
|
26
|
+
stripLongStrings(part);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Create debug data with metadata
|
|
30
|
+
const debugData = {
|
|
31
|
+
timestamp: new Date().toISOString(),
|
|
32
|
+
sessionID: state.sessionID,
|
|
33
|
+
events: eventsCopy,
|
|
34
|
+
metadata: {
|
|
35
|
+
command: "/debug",
|
|
36
|
+
version: "1.0",
|
|
37
|
+
totalEvents: eventsCopy.length,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// Ensure log dir exists
|
|
42
|
+
const logDir = getLogDir();
|
|
43
|
+
mkdirSync(logDir, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Create filename with timestamp
|
|
46
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
47
|
+
const filename = `debug-${timestamp}.json`;
|
|
48
|
+
const filepath = join(logDir, filename);
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
// Write to JSON file
|
|
52
|
+
writeFileSync(filepath, JSON.stringify(debugData, null, 2));
|
|
53
|
+
console.log(`✅ Debug data saved in ${logDir}`);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`❌ Failed to save debug data: ${error}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
console.log();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function stripLongStrings(target: Record<PropertyKey, unknown>): void {
|
|
62
|
+
for (const prop in target) {
|
|
63
|
+
if (prop !== "text" && prop !== "delta") {
|
|
64
|
+
const value = target[prop];
|
|
65
|
+
if (typeof value === "string") {
|
|
66
|
+
if (value.length > 255) {
|
|
67
|
+
target[prop] = value.substring(0, 252) + "...";
|
|
68
|
+
}
|
|
69
|
+
} else if (typeof value === "object" && value !== null) {
|
|
70
|
+
stripLongStrings(value as Record<PropertyKey, unknown>);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import type { State } from "../index";
|
|
3
|
+
import { render } from "../render";
|
|
4
|
+
import type { Command } from "../types";
|
|
5
|
+
|
|
6
|
+
let command: Command = {
|
|
7
|
+
name: "/details",
|
|
8
|
+
description: "Show all parts from the last request",
|
|
9
|
+
run,
|
|
10
|
+
running: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default command;
|
|
14
|
+
|
|
15
|
+
function run(_client: OpencodeClient, state: State): void {
|
|
16
|
+
render(state, true);
|
|
17
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import { config } from "../config";
|
|
3
|
+
import type { State } from "../index";
|
|
4
|
+
import type { Command } from "../types";
|
|
5
|
+
|
|
6
|
+
let command: Command = {
|
|
7
|
+
name: "/diff",
|
|
8
|
+
description: "Show file additions and deletions",
|
|
9
|
+
run,
|
|
10
|
+
running: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default command;
|
|
14
|
+
|
|
15
|
+
interface DiffLine {
|
|
16
|
+
type: "add" | "remove" | "same";
|
|
17
|
+
line: string;
|
|
18
|
+
oldIndex?: number;
|
|
19
|
+
newIndex?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function run(client: OpencodeClient, state: State): Promise<void> {
|
|
23
|
+
if (!config.sessionID) {
|
|
24
|
+
console.log("No active session.\n");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
console.log("Fetching file changes...");
|
|
29
|
+
|
|
30
|
+
const result = await client.session.diff({
|
|
31
|
+
path: { id: config.sessionID },
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (result.error) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Failed to fetch diff (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const allDiffs = result.data;
|
|
41
|
+
|
|
42
|
+
if (!allDiffs || allDiffs.length === 0) {
|
|
43
|
+
console.log("No file changes found.\n");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const file of allDiffs) {
|
|
48
|
+
console.log(`\x1b[36;1m${file.file}\x1b[0m`);
|
|
49
|
+
|
|
50
|
+
if (!file.before && file.after) {
|
|
51
|
+
console.log(`\x1b[32m+ new file\x1b[0m\n`);
|
|
52
|
+
const lines = file.after.split("\n");
|
|
53
|
+
for (let i = 0; i < lines.length; i++) {
|
|
54
|
+
console.log(`\x1b[90m${i + 1}\x1b[0m \x1b[32m+ ${lines[i]!}\x1b[0m`);
|
|
55
|
+
}
|
|
56
|
+
console.log();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (file.before && !file.after) {
|
|
61
|
+
console.log(`\x1b[31m- deleted file\x1b[0m\n`);
|
|
62
|
+
const lines = file.before.split("\n");
|
|
63
|
+
for (let i = 0; i < lines.length; i++) {
|
|
64
|
+
console.log(`\x1b[90m${i + 1}\x1b[0m \x1b[31m- ${lines[i]!}\x1b[0m`);
|
|
65
|
+
}
|
|
66
|
+
console.log();
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (file.before && file.after) {
|
|
71
|
+
const diff = computeDiff(file.before, file.after);
|
|
72
|
+
for (const diffLine of diff) {
|
|
73
|
+
if (diffLine.type === "add") {
|
|
74
|
+
console.log(`\x1b[90m${diffLine.newIndex! + 1}\x1b[0m \x1b[32m+ ${diffLine.line}\x1b[0m`);
|
|
75
|
+
} else if (diffLine.type === "remove") {
|
|
76
|
+
console.log(`\x1b[90m${diffLine.oldIndex! + 1}\x1b[0m \x1b[31m- ${diffLine.line}\x1b[0m`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
console.log();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function computeDiff(before: string, after: string): DiffLine[] {
|
|
85
|
+
const beforeLines = before.split("\n");
|
|
86
|
+
const afterLines = after.split("\n");
|
|
87
|
+
|
|
88
|
+
const lcs = computeLCS(beforeLines, afterLines);
|
|
89
|
+
|
|
90
|
+
const result: DiffLine[] = [];
|
|
91
|
+
let oldIdx = 0;
|
|
92
|
+
let newIdx = 0;
|
|
93
|
+
|
|
94
|
+
for (const lcsItem of lcs) {
|
|
95
|
+
while (oldIdx < lcsItem.oldIndex!) {
|
|
96
|
+
result.push({ type: "remove", line: beforeLines[oldIdx]!, oldIndex: oldIdx });
|
|
97
|
+
oldIdx++;
|
|
98
|
+
}
|
|
99
|
+
while (newIdx < lcsItem.newIndex!) {
|
|
100
|
+
result.push({ type: "add", line: afterLines[newIdx]!, newIndex: newIdx });
|
|
101
|
+
newIdx++;
|
|
102
|
+
}
|
|
103
|
+
result.push(lcsItem);
|
|
104
|
+
oldIdx++;
|
|
105
|
+
newIdx++;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
while (oldIdx < beforeLines.length) {
|
|
109
|
+
result.push({ type: "remove", line: beforeLines[oldIdx]!, oldIndex: oldIdx });
|
|
110
|
+
oldIdx++;
|
|
111
|
+
}
|
|
112
|
+
while (newIdx < afterLines.length) {
|
|
113
|
+
result.push({ type: "add", line: afterLines[newIdx]!, newIndex: newIdx });
|
|
114
|
+
newIdx++;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function computeLCS(beforeLines: string[], afterLines: string[]): DiffLine[] {
|
|
121
|
+
const m = beforeLines.length;
|
|
122
|
+
const n = afterLines.length;
|
|
123
|
+
|
|
124
|
+
const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
125
|
+
|
|
126
|
+
for (let i = 1; i <= m; i++) {
|
|
127
|
+
for (let j = 1; j <= n; j++) {
|
|
128
|
+
if (beforeLines[i - 1] === afterLines[j - 1]) {
|
|
129
|
+
dp[i]![j] = dp[i - 1]![j - 1]! + 1;
|
|
130
|
+
} else {
|
|
131
|
+
dp[i]![j] = Math.max(dp[i - 1]![j]!, dp[i]![j - 1]!);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const result: DiffLine[] = [];
|
|
137
|
+
let i = m;
|
|
138
|
+
let j = n;
|
|
139
|
+
|
|
140
|
+
while (i > 0 || j > 0) {
|
|
141
|
+
if (i > 0 && j > 0 && beforeLines[i - 1] === afterLines[j - 1]) {
|
|
142
|
+
result.unshift({ type: "same", line: beforeLines[i - 1]!, oldIndex: i - 1, newIndex: j - 1 });
|
|
143
|
+
i--;
|
|
144
|
+
j--;
|
|
145
|
+
} else if (j > 0 && (i === 0 || dp[i]![j]! === dp[i]![j - 1]!)) {
|
|
146
|
+
result.unshift({ type: "add", line: afterLines[j - 1]!, newIndex: j - 1 });
|
|
147
|
+
j--;
|
|
148
|
+
} else if (i > 0) {
|
|
149
|
+
result.unshift({ type: "remove", line: beforeLines[i - 1]!, oldIndex: i - 1 });
|
|
150
|
+
i--;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import type { State } from "../index";
|
|
3
|
+
import type { Command } from "../types";
|
|
4
|
+
|
|
5
|
+
let command: Command = {
|
|
6
|
+
name: "/exit",
|
|
7
|
+
description: "Exit the application",
|
|
8
|
+
run,
|
|
9
|
+
running: false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default command;
|
|
13
|
+
|
|
14
|
+
async function run(_client: OpencodeClient, _state: State): Promise<void> {
|
|
15
|
+
console.log(`\x1b[90mGoodbye!\x1b[0m`);
|
|
16
|
+
process.exit(0);
|
|
17
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import { config, saveConfig } from "../config";
|
|
3
|
+
import type { State } from "../index";
|
|
4
|
+
import { writePrompt } from "../render";
|
|
5
|
+
import type { Command } from "../types";
|
|
6
|
+
|
|
7
|
+
let command: Command = {
|
|
8
|
+
name: "/init",
|
|
9
|
+
description: "Analyze project and create/update AGENTS.md",
|
|
10
|
+
run,
|
|
11
|
+
running: false,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export default command;
|
|
15
|
+
|
|
16
|
+
async function run(_client: OpencodeClient, _state: State): Promise<void> {
|
|
17
|
+
if (!config.sessionID) return;
|
|
18
|
+
|
|
19
|
+
console.log("Running /init command (analyzing project and creating AGENTS.md)...");
|
|
20
|
+
const result = await _client.session.init({
|
|
21
|
+
path: { id: config.sessionID },
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (result.error) {
|
|
25
|
+
throw new Error(
|
|
26
|
+
`Failed to run /init (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
console.log();
|
|
31
|
+
console.log(
|
|
32
|
+
result.data ? "AGENTS.md created/updated successfully." : "No changes made to AGENTS.md.",
|
|
33
|
+
);
|
|
34
|
+
console.log();
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import type { State } from "../index";
|
|
3
|
+
import type { Command } from "../types";
|
|
4
|
+
|
|
5
|
+
let command: Command = {
|
|
6
|
+
name: "/kill",
|
|
7
|
+
description: "Abort a session (e.g. `/kill ses_123`)",
|
|
8
|
+
run,
|
|
9
|
+
running: false,
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export default command;
|
|
13
|
+
|
|
14
|
+
async function run(client: OpencodeClient, _state: State, input?: string): Promise<void> {
|
|
15
|
+
if (!input) {
|
|
16
|
+
console.log("Usage: /kill <session_id>");
|
|
17
|
+
console.log();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = await client.session.abort({
|
|
22
|
+
path: { id: input },
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (result.error) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Failed to abort session (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
console.log(`Session aborted successfully.`);
|
|
32
|
+
console.log();
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import { config, saveConfig } from "../config";
|
|
3
|
+
import type { State } from "../index";
|
|
4
|
+
import type { Command } from "../types";
|
|
5
|
+
|
|
6
|
+
let command: Command = {
|
|
7
|
+
name: "/log",
|
|
8
|
+
description: "Toggle logging of parts to file",
|
|
9
|
+
run,
|
|
10
|
+
running: false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export default command;
|
|
14
|
+
|
|
15
|
+
export function isLoggingEnabled(): boolean {
|
|
16
|
+
return config.loggingEnabled;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function run(_client: OpencodeClient, _state: State): void {
|
|
20
|
+
config.loggingEnabled = !config.loggingEnabled;
|
|
21
|
+
saveConfig();
|
|
22
|
+
const status = config.loggingEnabled ? "enabled" : "disabled";
|
|
23
|
+
console.log(`📝 Logging ${status}\n`);
|
|
24
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import readline, { type Key } from "node:readline";
|
|
3
|
+
import { config, saveConfig } from "../config";
|
|
4
|
+
import { getActiveDisplay, writePrompt } from "../render";
|
|
5
|
+
import type { Command } from "../types";
|
|
6
|
+
|
|
7
|
+
let command: Command = {
|
|
8
|
+
name: "/models",
|
|
9
|
+
description: "List and select available models",
|
|
10
|
+
run,
|
|
11
|
+
handleKey,
|
|
12
|
+
running: false,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default command;
|
|
16
|
+
|
|
17
|
+
interface ModelInfo {
|
|
18
|
+
providerID: string;
|
|
19
|
+
providerName: string;
|
|
20
|
+
modelID: string;
|
|
21
|
+
modelName: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let modelList: ModelInfo[] = [];
|
|
25
|
+
let selectedModelIndex = 0;
|
|
26
|
+
let modelListLineCount = 0;
|
|
27
|
+
let modelSearchString = "";
|
|
28
|
+
let modelFilteredIndices: number[] = [];
|
|
29
|
+
|
|
30
|
+
async function run(client: OpencodeClient): Promise<void> {
|
|
31
|
+
const result = await client.config.providers();
|
|
32
|
+
|
|
33
|
+
if (result.error) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Failed to fetch models (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const providers = result.data?.providers || [];
|
|
40
|
+
|
|
41
|
+
modelList = [];
|
|
42
|
+
for (const provider of providers) {
|
|
43
|
+
const models = Object.values(provider.models || {});
|
|
44
|
+
for (const model of models) {
|
|
45
|
+
modelList.push({
|
|
46
|
+
providerID: provider.id,
|
|
47
|
+
providerName: provider.name,
|
|
48
|
+
modelID: model.id,
|
|
49
|
+
modelName: model.name || model.id,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
modelList.sort(
|
|
55
|
+
(a, b) =>
|
|
56
|
+
a.providerName.localeCompare(b.providerName) || a.modelName.localeCompare(b.modelName),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
modelSearchString = "";
|
|
60
|
+
updateModelFilter();
|
|
61
|
+
|
|
62
|
+
command.running = true;
|
|
63
|
+
|
|
64
|
+
renderModelList();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
async function handleKey(client: OpencodeClient, key: Key, str?: string) {
|
|
68
|
+
switch (key.name) {
|
|
69
|
+
case "up": {
|
|
70
|
+
if (selectedModelIndex === 0) {
|
|
71
|
+
selectedModelIndex = modelFilteredIndices.length - 1;
|
|
72
|
+
} else {
|
|
73
|
+
selectedModelIndex--;
|
|
74
|
+
}
|
|
75
|
+
renderModelList();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
case "down": {
|
|
79
|
+
if (selectedModelIndex === modelFilteredIndices.length - 1) {
|
|
80
|
+
selectedModelIndex = 0;
|
|
81
|
+
} else {
|
|
82
|
+
selectedModelIndex++;
|
|
83
|
+
}
|
|
84
|
+
renderModelList();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
case "escape": {
|
|
88
|
+
clearModelList();
|
|
89
|
+
process.stdout.write("\x1b[?25h");
|
|
90
|
+
command.running = false;
|
|
91
|
+
modelList = [];
|
|
92
|
+
selectedModelIndex = 0;
|
|
93
|
+
modelListLineCount = 0;
|
|
94
|
+
modelSearchString = "";
|
|
95
|
+
modelFilteredIndices = [];
|
|
96
|
+
readline.cursorTo(process.stdout, 0);
|
|
97
|
+
readline.clearScreenDown(process.stdout);
|
|
98
|
+
writePrompt();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
case "return": {
|
|
102
|
+
modelListLineCount++;
|
|
103
|
+
clearModelList();
|
|
104
|
+
process.stdout.write("\x1b[?25h");
|
|
105
|
+
const selectedIndex = modelFilteredIndices[selectedModelIndex];
|
|
106
|
+
const selected = selectedIndex !== undefined ? modelList[selectedIndex] : undefined;
|
|
107
|
+
command.running = false;
|
|
108
|
+
modelList = [];
|
|
109
|
+
selectedModelIndex = 0;
|
|
110
|
+
modelListLineCount = 0;
|
|
111
|
+
modelSearchString = "";
|
|
112
|
+
modelFilteredIndices = [];
|
|
113
|
+
readline.cursorTo(process.stdout, 0);
|
|
114
|
+
readline.clearScreenDown(process.stdout);
|
|
115
|
+
if (selected) {
|
|
116
|
+
config.providerID = selected.providerID;
|
|
117
|
+
config.modelID = selected.modelID;
|
|
118
|
+
saveConfig();
|
|
119
|
+
const activeDisplay = await getActiveDisplay(client);
|
|
120
|
+
console.log(activeDisplay);
|
|
121
|
+
console.log();
|
|
122
|
+
}
|
|
123
|
+
writePrompt();
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
case "backspace": {
|
|
127
|
+
modelSearchString = modelSearchString.slice(0, -1);
|
|
128
|
+
updateModelFilter();
|
|
129
|
+
selectedModelIndex = 0;
|
|
130
|
+
renderModelList();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (str && str.length === 1) {
|
|
136
|
+
modelSearchString += str;
|
|
137
|
+
updateModelFilter();
|
|
138
|
+
selectedModelIndex = 0;
|
|
139
|
+
renderModelList();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function clearModelList() {
|
|
145
|
+
process.stdout.write("\x1b[?25l");
|
|
146
|
+
if (modelListLineCount > 0) {
|
|
147
|
+
process.stdout.write(`\x1b[${modelListLineCount}A`);
|
|
148
|
+
}
|
|
149
|
+
readline.cursorTo(process.stdout, 0);
|
|
150
|
+
readline.clearScreenDown(process.stdout);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function renderModelList(): void {
|
|
154
|
+
clearModelList();
|
|
155
|
+
|
|
156
|
+
const grouped = new Map<string, { models: typeof modelList; startIndices: number[] }>();
|
|
157
|
+
let currentIndex = 0;
|
|
158
|
+
for (const model of modelList) {
|
|
159
|
+
const existing = grouped.get(model.providerName);
|
|
160
|
+
if (existing) {
|
|
161
|
+
existing.models.push(model);
|
|
162
|
+
existing.startIndices.push(currentIndex);
|
|
163
|
+
} else {
|
|
164
|
+
grouped.set(model.providerName, { models: [model], startIndices: [currentIndex] });
|
|
165
|
+
}
|
|
166
|
+
currentIndex++;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
modelListLineCount = 0;
|
|
170
|
+
if (modelSearchString) {
|
|
171
|
+
console.log(` \x1b[90mFilter: \x1b[0m\x1b[33m${modelSearchString}\x1b[0m`);
|
|
172
|
+
modelListLineCount++;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const [providerName, data] of grouped) {
|
|
176
|
+
const filteredModelsWithIndices = data.models
|
|
177
|
+
.map((model, i) => ({ model, globalIndex: data.startIndices[i]! }))
|
|
178
|
+
.filter(({ globalIndex }) => modelFilteredIndices.includes(globalIndex));
|
|
179
|
+
|
|
180
|
+
if (filteredModelsWithIndices.length === 0) continue;
|
|
181
|
+
|
|
182
|
+
console.log(` \x1b[36;1m${providerName}\x1b[0m`);
|
|
183
|
+
modelListLineCount++;
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < filteredModelsWithIndices.length; i++) {
|
|
186
|
+
const { model, globalIndex } = filteredModelsWithIndices[i]!;
|
|
187
|
+
const filteredIndex = modelFilteredIndices.indexOf(globalIndex);
|
|
188
|
+
const isSelected = filteredIndex === selectedModelIndex;
|
|
189
|
+
const isActive = model.providerID === config.providerID && model.modelID === config.modelID;
|
|
190
|
+
const prefix = isSelected ? " >" : " -";
|
|
191
|
+
const name = isSelected ? `\x1b[33;1m${model.modelName}\x1b[0m` : model.modelName;
|
|
192
|
+
const status = isActive ? " (active)" : "";
|
|
193
|
+
|
|
194
|
+
console.log(`${prefix} ${name}${status}`);
|
|
195
|
+
modelListLineCount++;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function updateModelFilter(): void {
|
|
201
|
+
if (!modelSearchString) {
|
|
202
|
+
modelFilteredIndices = modelList.map((_, i) => i);
|
|
203
|
+
} else {
|
|
204
|
+
const search = modelSearchString.toLowerCase();
|
|
205
|
+
modelFilteredIndices = modelList
|
|
206
|
+
.map((model, i) => ({ model, index: i }))
|
|
207
|
+
.filter(({ model }) => model.modelName.toLowerCase().includes(search))
|
|
208
|
+
.map(({ index }) => index);
|
|
209
|
+
}
|
|
210
|
+
if (modelFilteredIndices.length > 0) {
|
|
211
|
+
selectedModelIndex = modelFilteredIndices.indexOf(
|
|
212
|
+
modelList.findIndex(
|
|
213
|
+
(m) => m.providerID === config.providerID && m.modelID === config.modelID,
|
|
214
|
+
),
|
|
215
|
+
);
|
|
216
|
+
if (selectedModelIndex === -1) selectedModelIndex = 0;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import { config, saveConfig } from "../config";
|
|
3
|
+
import type { State } from "../index";
|
|
4
|
+
import { updateSessionTitle } from "../index";
|
|
5
|
+
import { getActiveDisplay } from "../render";
|
|
6
|
+
import type { Command } from "../types";
|
|
7
|
+
|
|
8
|
+
let command: Command = {
|
|
9
|
+
name: "/new",
|
|
10
|
+
description: "Create a new session",
|
|
11
|
+
run,
|
|
12
|
+
running: false,
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export default command;
|
|
16
|
+
|
|
17
|
+
async function run(client: OpencodeClient, state: State): Promise<void> {
|
|
18
|
+
state.sessionID = await createSession(client);
|
|
19
|
+
config.sessionID = state.sessionID;
|
|
20
|
+
saveConfig();
|
|
21
|
+
|
|
22
|
+
await updateSessionTitle();
|
|
23
|
+
|
|
24
|
+
const activeDisplay = await getActiveDisplay(client);
|
|
25
|
+
console.log(activeDisplay);
|
|
26
|
+
console.log(`Created new session`);
|
|
27
|
+
console.log();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function createSession(client: OpencodeClient): Promise<string> {
|
|
31
|
+
const result = await client.session.create({
|
|
32
|
+
body: {},
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (result.error) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Failed to create session (${result.response.status}): ${JSON.stringify(result.error)}`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return result.data.id;
|
|
42
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { OpencodeClient } from "@opencode-ai/sdk";
|
|
2
|
+
import type { Key } from "node:readline";
|
|
3
|
+
import type { State } from "../index";
|
|
4
|
+
import { wrapText } from "../render";
|
|
5
|
+
import type { Command } from "../types";
|
|
6
|
+
|
|
7
|
+
let currentPageIndex = 0;
|
|
8
|
+
let pages: string[] = [];
|
|
9
|
+
|
|
10
|
+
let command: Command = {
|
|
11
|
+
name: "/page",
|
|
12
|
+
description: "Show detailed output page by page",
|
|
13
|
+
run,
|
|
14
|
+
handleKey,
|
|
15
|
+
running: false,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export default command;
|
|
19
|
+
|
|
20
|
+
function run(client: OpencodeClient, state: State): void {
|
|
21
|
+
pages = [];
|
|
22
|
+
|
|
23
|
+
for (const part of state.accumulatedResponse) {
|
|
24
|
+
if (!part || !part.text.trim()) continue;
|
|
25
|
+
|
|
26
|
+
if (part.title === "thinking") {
|
|
27
|
+
pages.push(`💭 \x1b[90m${part.text.trimStart()}\x1b[0m`);
|
|
28
|
+
} else if (part.title === "response") {
|
|
29
|
+
pages.push(`💬 ${part.text.trimStart()}`);
|
|
30
|
+
} else if (part.title === "tool") {
|
|
31
|
+
pages.push(part.text);
|
|
32
|
+
} else if (part.title === "files") {
|
|
33
|
+
pages.push(part.text);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (pages.length === 0) {
|
|
38
|
+
console.log("\n\x1b[90mNo parts to display yet.\x1b[0m\n");
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
currentPageIndex = 0;
|
|
43
|
+
command.running = true;
|
|
44
|
+
|
|
45
|
+
displayPage();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function handleKey(client: OpencodeClient, key: Key, _input?: string): void {
|
|
49
|
+
if (key.name === "space") {
|
|
50
|
+
currentPageIndex++;
|
|
51
|
+
if (currentPageIndex >= pages.length) {
|
|
52
|
+
command.running = false;
|
|
53
|
+
process.stdout.write("\x1b[?25h");
|
|
54
|
+
process.stdout.write("\x1b[2K\r\n");
|
|
55
|
+
} else {
|
|
56
|
+
displayPage();
|
|
57
|
+
}
|
|
58
|
+
} else if (key.name === "escape") {
|
|
59
|
+
command.running = false;
|
|
60
|
+
process.stdout.write("\x1b[?25h");
|
|
61
|
+
console.log("\n\x1b[90mCancelled\x1b[0m\n");
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function displayPage(): void {
|
|
66
|
+
let page = pages[currentPageIndex]!;
|
|
67
|
+
if (process.stdout.columns) {
|
|
68
|
+
page = wrapText(page, process.stdout.columns).join("\n");
|
|
69
|
+
}
|
|
70
|
+
const footer = `\n\n\x1b[90m--- Part ${currentPageIndex + 1} of ${pages.length}; press SPACE to advance or ESC to cancel ---\x1b[0m`;
|
|
71
|
+
|
|
72
|
+
if (currentPageIndex > 0) {
|
|
73
|
+
process.stdout.write("\x1b[2K\r");
|
|
74
|
+
process.stdout.write(page + footer);
|
|
75
|
+
} else {
|
|
76
|
+
process.stdout.write(page + footer);
|
|
77
|
+
}
|
|
78
|
+
}
|