@duckmind/dm-darwin-arm64 0.13.6 → 0.13.7
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/extensions/.dm-extensions.json +25 -1
- package/extensions/dm-phone/README.md +23 -0
- package/extensions/dm-phone/index.ts +12 -0
- package/extensions/dm-phone/node_modules/.package-lock.json +29 -0
- package/extensions/dm-phone/node_modules/ws/LICENSE +20 -0
- package/extensions/dm-phone/node_modules/ws/README.md +548 -0
- package/extensions/dm-phone/node_modules/ws/browser.js +8 -0
- package/extensions/dm-phone/node_modules/ws/index.js +22 -0
- package/extensions/dm-phone/node_modules/ws/lib/buffer-util.js +131 -0
- package/extensions/dm-phone/node_modules/ws/lib/constants.js +19 -0
- package/extensions/dm-phone/node_modules/ws/lib/event-target.js +292 -0
- package/extensions/dm-phone/node_modules/ws/lib/extension.js +203 -0
- package/extensions/dm-phone/node_modules/ws/lib/limiter.js +55 -0
- package/extensions/dm-phone/node_modules/ws/lib/permessage-deflate.js +528 -0
- package/extensions/dm-phone/node_modules/ws/lib/receiver.js +706 -0
- package/extensions/dm-phone/node_modules/ws/lib/sender.js +602 -0
- package/extensions/dm-phone/node_modules/ws/lib/stream.js +161 -0
- package/extensions/dm-phone/node_modules/ws/lib/subprotocol.js +62 -0
- package/extensions/dm-phone/node_modules/ws/lib/validation.js +152 -0
- package/extensions/dm-phone/node_modules/ws/lib/websocket-server.js +554 -0
- package/extensions/dm-phone/node_modules/ws/lib/websocket.js +1393 -0
- package/extensions/dm-phone/node_modules/ws/package.json +70 -0
- package/extensions/dm-phone/node_modules/ws/wrapper.mjs +21 -0
- package/extensions/dm-phone/package-lock.json +66 -0
- package/extensions/dm-phone/package.json +35 -0
- package/extensions/dm-phone/phone-session-pool.ts +8 -0
- package/extensions/dm-phone/public/app/attachments.js +233 -0
- package/extensions/dm-phone/public/app/autocomplete-controller.js +81 -0
- package/extensions/dm-phone/public/app/autocomplete.js +135 -0
- package/extensions/dm-phone/public/app/bindings.js +178 -0
- package/extensions/dm-phone/public/app/command-catalog.js +76 -0
- package/extensions/dm-phone/public/app/commands.js +370 -0
- package/extensions/dm-phone/public/app/constants.js +60 -0
- package/extensions/dm-phone/public/app/formatters.js +131 -0
- package/extensions/dm-phone/public/app/handlers.js +442 -0
- package/extensions/dm-phone/public/app/main.js +6 -0
- package/extensions/dm-phone/public/app/markdown.js +105 -0
- package/extensions/dm-phone/public/app/messages.js +418 -0
- package/extensions/dm-phone/public/app/sheet-actions.js +113 -0
- package/extensions/dm-phone/public/app/sheet-navigation.js +19 -0
- package/extensions/dm-phone/public/app/sheets-view.js +272 -0
- package/extensions/dm-phone/public/app/state.js +95 -0
- package/extensions/dm-phone/public/app/tool-rendering.js +562 -0
- package/extensions/dm-phone/public/app/transport.js +176 -0
- package/extensions/dm-phone/public/app/ui.js +409 -0
- package/extensions/dm-phone/public/app.js +1 -0
- package/extensions/dm-phone/public/icon.svg +15 -0
- package/extensions/dm-phone/public/index.html +147 -0
- package/extensions/dm-phone/public/manifest.webmanifest +17 -0
- package/extensions/dm-phone/public/styles.css +1139 -0
- package/extensions/dm-phone/public/sw.js +78 -0
- package/extensions/dm-phone/src/extension/phone-args.ts +121 -0
- package/extensions/dm-phone/src/extension/phone-paths.ts +250 -0
- package/extensions/dm-phone/src/extension/phone-quota.ts +188 -0
- package/extensions/dm-phone/src/extension/phone-runtime.ts +154 -0
- package/extensions/dm-phone/src/extension/phone-server-runtime.ts +1217 -0
- package/extensions/dm-phone/src/extension/phone-sessions.ts +139 -0
- package/extensions/dm-phone/src/extension/phone-static.ts +30 -0
- package/extensions/dm-phone/src/extension/phone-tailscale.ts +148 -0
- package/extensions/dm-phone/src/extension/phone-theme.ts +85 -0
- package/extensions/dm-phone/src/extension/register-phone-child-extension.ts +112 -0
- package/extensions/dm-phone/src/extension/register-phone-extension.ts +106 -0
- package/extensions/dm-phone/src/extension/types.ts +73 -0
- package/extensions/dm-phone/src/session-pool/parent-session-worker.ts +881 -0
- package/extensions/dm-phone/src/session-pool/session-pool.ts +470 -0
- package/extensions/dm-phone/src/session-pool/session-worker.ts +734 -0
- package/extensions/dm-phone/src/session-pool/types.ts +105 -0
- package/extensions/dm-phone/src/session-pool/utils.ts +23 -0
- package/package.json +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import type { SessionEntry } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { SessionManager } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
export function summarizeSessionEntry(entry: SessionEntry): {
|
|
5
|
+
kind: string;
|
|
6
|
+
preview: string;
|
|
7
|
+
role?: string;
|
|
8
|
+
} {
|
|
9
|
+
if (entry.type === "message") {
|
|
10
|
+
const message: any = entry.message;
|
|
11
|
+
if (message.role === "user") {
|
|
12
|
+
const preview = typeof message.content === "string"
|
|
13
|
+
? message.content
|
|
14
|
+
: Array.isArray(message.content)
|
|
15
|
+
? message.content
|
|
16
|
+
.map((part: any) => (part.type === "text" ? part.text || "" : part.type === "image" ? "[image]" : ""))
|
|
17
|
+
.join(" ")
|
|
18
|
+
: "";
|
|
19
|
+
return { kind: "message", role: "user", preview: preview || "(user message)" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (message.role === "assistant") {
|
|
23
|
+
const preview = Array.isArray(message.content)
|
|
24
|
+
? message.content
|
|
25
|
+
.map((part: any) => (part.type === "text" ? part.text || "" : part.type === "toolCall" ? `[tool:${part.name || "tool"}]` : ""))
|
|
26
|
+
.join(" ")
|
|
27
|
+
: "";
|
|
28
|
+
return { kind: "message", role: "assistant", preview: preview || "(assistant message)" };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (message.role === "toolResult") {
|
|
32
|
+
const preview = Array.isArray(message.content)
|
|
33
|
+
? message.content.map((part: any) => (part.type === "text" ? part.text || "" : "")).join(" ")
|
|
34
|
+
: "";
|
|
35
|
+
return { kind: "tool", role: message.toolName || "tool", preview: preview || `(${message.toolName || "tool"} result)` };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (message.role === "custom") {
|
|
39
|
+
return {
|
|
40
|
+
kind: "custom",
|
|
41
|
+
role: message.customType || "custom",
|
|
42
|
+
preview: typeof message.content === "string" ? message.content : "(custom message)",
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (message.role === "branchSummary") {
|
|
47
|
+
return { kind: "summary", role: "branchSummary", preview: message.summary || "(branch summary)" };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (message.role === "compactionSummary") {
|
|
51
|
+
return { kind: "summary", role: "compactionSummary", preview: message.summary || "(compaction summary)" };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return { kind: "message", role: message.role, preview: `(${message.role || "message"})` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (entry.type === "compaction") {
|
|
58
|
+
return { kind: "compaction", preview: entry.summary || "(compaction)" };
|
|
59
|
+
}
|
|
60
|
+
if (entry.type === "branch_summary") {
|
|
61
|
+
return { kind: "branch_summary", preview: entry.summary || "(branch summary)" };
|
|
62
|
+
}
|
|
63
|
+
if (entry.type === "session_info") {
|
|
64
|
+
return { kind: "session_info", preview: entry.name || "(session info)" };
|
|
65
|
+
}
|
|
66
|
+
if (entry.type === "label") {
|
|
67
|
+
return { kind: "label", preview: entry.label || "(label cleared)" };
|
|
68
|
+
}
|
|
69
|
+
if (entry.type === "model_change") {
|
|
70
|
+
return { kind: "model_change", preview: `${entry.provider}/${entry.modelId}` };
|
|
71
|
+
}
|
|
72
|
+
if (entry.type === "thinking_level_change") {
|
|
73
|
+
return { kind: "thinking_level_change", preview: entry.thinkingLevel || "(thinking change)" };
|
|
74
|
+
}
|
|
75
|
+
if (entry.type === "custom") {
|
|
76
|
+
return { kind: "custom", preview: entry.customType || "(custom entry)" };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return { kind: entry.type, preview: `(${entry.type})` };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function flattenTreeNode(node: any, depth = 0, out: any[] = []): any[] {
|
|
83
|
+
const summary = summarizeSessionEntry(node.entry as SessionEntry);
|
|
84
|
+
out.push({
|
|
85
|
+
id: node.entry.id,
|
|
86
|
+
parentId: node.entry.parentId,
|
|
87
|
+
type: node.entry.type,
|
|
88
|
+
depth,
|
|
89
|
+
timestamp: node.entry.timestamp,
|
|
90
|
+
label: node.label,
|
|
91
|
+
childCount: Array.isArray(node.children) ? node.children.length : 0,
|
|
92
|
+
summary,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
for (const childNode of node.children || []) {
|
|
96
|
+
flattenTreeNode(childNode, depth + 1, out);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function listSessionsForCwd(cwd: string) {
|
|
103
|
+
const sessions = await SessionManager.list(cwd);
|
|
104
|
+
return sessions.map((session) => ({
|
|
105
|
+
path: session.path,
|
|
106
|
+
id: session.id,
|
|
107
|
+
cwd: session.cwd,
|
|
108
|
+
name: session.name,
|
|
109
|
+
parentSessionPath: session.parentSessionPath,
|
|
110
|
+
created: session.created,
|
|
111
|
+
modified: session.modified,
|
|
112
|
+
messageCount: session.messageCount,
|
|
113
|
+
firstMessage: session.firstMessage,
|
|
114
|
+
}));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export function getTreeStateFromSessionFile(sessionFile: string) {
|
|
118
|
+
const sessionManager = SessionManager.open(sessionFile);
|
|
119
|
+
const branch = sessionManager.getBranch();
|
|
120
|
+
const currentPathIds = new Set(branch.map((entry) => entry.id));
|
|
121
|
+
const roots = sessionManager.getTree();
|
|
122
|
+
const nodes = roots.flatMap((root) => flattenTreeNode(root));
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
sessionFile,
|
|
126
|
+
currentLeafId: sessionManager.getLeafId(),
|
|
127
|
+
currentPathIds: [...currentPathIds],
|
|
128
|
+
nodes,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function createBranchSessionFromEntry(sessionFile: string, entryId: string) {
|
|
133
|
+
const sessionManager = SessionManager.open(sessionFile);
|
|
134
|
+
const nextPath = sessionManager.createBranchedSession(entryId);
|
|
135
|
+
if (!nextPath) {
|
|
136
|
+
throw new Error("Failed to create branch session.");
|
|
137
|
+
}
|
|
138
|
+
return nextPath;
|
|
139
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { dirname, join, normalize, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
5
|
+
const __dirname = dirname(__filename);
|
|
6
|
+
|
|
7
|
+
export const publicDir = resolve(__dirname, "../../public");
|
|
8
|
+
|
|
9
|
+
export const mimeTypes: Record<string, string> = {
|
|
10
|
+
".css": "text/css; charset=utf-8",
|
|
11
|
+
".html": "text/html; charset=utf-8",
|
|
12
|
+
".ico": "image/x-icon",
|
|
13
|
+
".js": "application/javascript; charset=utf-8",
|
|
14
|
+
".json": "application/json; charset=utf-8",
|
|
15
|
+
".png": "image/png",
|
|
16
|
+
".svg": "image/svg+xml",
|
|
17
|
+
".txt": "text/plain; charset=utf-8",
|
|
18
|
+
".webmanifest": "application/manifest+json; charset=utf-8",
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function sanitizePublicPath(pathname: string): string | null {
|
|
22
|
+
const normalized = normalize(pathname).replace(/^\/+/, "");
|
|
23
|
+
const filePath = resolve(publicDir, normalized === "" ? "index.html" : normalized);
|
|
24
|
+
if (!filePath.startsWith(publicDir)) return null;
|
|
25
|
+
return filePath;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function publicFilePath(relativePath: string): string {
|
|
29
|
+
return join(publicDir, relativePath);
|
|
30
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
function isPhoneServeProxyTarget(proxy: string, port: number) {
|
|
4
|
+
try {
|
|
5
|
+
const url = new URL(proxy);
|
|
6
|
+
return url.protocol === "http:" && url.port === String(port) && ["127.0.0.1", "localhost", "::1", "[::1]"].includes(url.hostname);
|
|
7
|
+
} catch {
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getTailscaleUrl(pi: ExtensionAPI) {
|
|
13
|
+
try {
|
|
14
|
+
const status = await pi.exec("tailscale", ["status", "--json"], { timeout: 5000 });
|
|
15
|
+
if (status.code !== 0) return "";
|
|
16
|
+
|
|
17
|
+
const payload = JSON.parse(status.stdout || "{}");
|
|
18
|
+
const dnsName = typeof payload?.Self?.DNSName === "string" ? payload.Self.DNSName.replace(/\.$/, "") : "";
|
|
19
|
+
return dnsName ? `https://${dnsName}/` : "";
|
|
20
|
+
} catch {
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function getTailscaleServeInfo(pi: ExtensionAPI, port: number) {
|
|
26
|
+
const url = await getTailscaleUrl(pi);
|
|
27
|
+
try {
|
|
28
|
+
const status = await pi.exec("tailscale", ["serve", "status", "--json"], { timeout: 5000 });
|
|
29
|
+
if (status.code !== 0) {
|
|
30
|
+
return {
|
|
31
|
+
active: false,
|
|
32
|
+
url,
|
|
33
|
+
hadAnyWebConfig: false,
|
|
34
|
+
error: (status.stderr || status.stdout || `tailscale serve status exited ${status.code}`).trim(),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let payload: any;
|
|
39
|
+
try {
|
|
40
|
+
payload = JSON.parse(status.stdout || "{}");
|
|
41
|
+
} catch {
|
|
42
|
+
return {
|
|
43
|
+
active: false,
|
|
44
|
+
url,
|
|
45
|
+
hadAnyWebConfig: false,
|
|
46
|
+
error: "Failed to parse tailscale serve status output.",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const services = Object.values(payload?.Web || {}) as any[];
|
|
51
|
+
let active = false;
|
|
52
|
+
for (const service of services) {
|
|
53
|
+
const handlers = service?.Handlers || {};
|
|
54
|
+
for (const handler of Object.values(handlers) as any[]) {
|
|
55
|
+
if (typeof handler?.Proxy === "string" && isPhoneServeProxyTarget(handler.Proxy, port)) {
|
|
56
|
+
active = true;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (active) break;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
active,
|
|
65
|
+
url,
|
|
66
|
+
hadAnyWebConfig: services.length > 0,
|
|
67
|
+
error: "",
|
|
68
|
+
};
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return {
|
|
71
|
+
active: false,
|
|
72
|
+
url,
|
|
73
|
+
hadAnyWebConfig: false,
|
|
74
|
+
error: error instanceof Error ? error.message : String(error),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export async function enableTailscaleServe(pi: ExtensionAPI, port: number) {
|
|
80
|
+
const before = await getTailscaleServeInfo(pi, port);
|
|
81
|
+
if (before.active) {
|
|
82
|
+
return {
|
|
83
|
+
enabled: true,
|
|
84
|
+
changed: false,
|
|
85
|
+
replacedExisting: false,
|
|
86
|
+
url: before.url,
|
|
87
|
+
error: "",
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const target = `http://127.0.0.1:${port}`;
|
|
93
|
+
const result = await pi.exec("tailscale", ["serve", "--bg", "--yes", "--https=443", target], { timeout: 5000 });
|
|
94
|
+
if (result.code !== 0) {
|
|
95
|
+
return {
|
|
96
|
+
enabled: false,
|
|
97
|
+
changed: false,
|
|
98
|
+
replacedExisting: before.hadAnyWebConfig,
|
|
99
|
+
url: before.url,
|
|
100
|
+
error: (result.stderr || result.stdout || `tailscale serve exited ${result.code}`).trim(),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const after = await getTailscaleServeInfo(pi, port);
|
|
105
|
+
return {
|
|
106
|
+
enabled: after.active || !after.error,
|
|
107
|
+
changed: true,
|
|
108
|
+
replacedExisting: before.hadAnyWebConfig,
|
|
109
|
+
url: after.url || before.url,
|
|
110
|
+
error: after.active ? "" : after.error,
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
enabled: false,
|
|
115
|
+
changed: false,
|
|
116
|
+
replacedExisting: before.hadAnyWebConfig,
|
|
117
|
+
url: before.url,
|
|
118
|
+
error: error instanceof Error ? error.message : String(error),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export async function disableMatchingTailscaleServe(pi: ExtensionAPI, port: number) {
|
|
124
|
+
const info = await getTailscaleServeInfo(pi, port);
|
|
125
|
+
if (!info.active) {
|
|
126
|
+
return {
|
|
127
|
+
disabled: false,
|
|
128
|
+
error: info.error,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const off = await pi.exec("tailscale", ["serve", "--yes", "--https=443", "off"], { timeout: 5000 });
|
|
134
|
+
if (off.code === 0) {
|
|
135
|
+
return { disabled: true, error: "" };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
disabled: false,
|
|
140
|
+
error: (off.stderr || off.stdout || `tailscale serve --https=443 off exited ${off.code}`).trim(),
|
|
141
|
+
};
|
|
142
|
+
} catch (error) {
|
|
143
|
+
return {
|
|
144
|
+
disabled: false,
|
|
145
|
+
error: error instanceof Error ? error.message : String(error),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
type ThemeLike = {
|
|
2
|
+
name?: string;
|
|
3
|
+
getFgAnsi: (...args: any[]) => string | undefined;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
function rgbToHex(r: number, g: number, b: number) {
|
|
7
|
+
return `#${[r, g, b].map((value) => value.toString(16).padStart(2, "0")).join("")}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function xterm256ToHex(index: number) {
|
|
11
|
+
const ansi16 = [
|
|
12
|
+
"#000000",
|
|
13
|
+
"#800000",
|
|
14
|
+
"#008000",
|
|
15
|
+
"#808000",
|
|
16
|
+
"#000080",
|
|
17
|
+
"#800080",
|
|
18
|
+
"#008080",
|
|
19
|
+
"#c0c0c0",
|
|
20
|
+
"#808080",
|
|
21
|
+
"#ff0000",
|
|
22
|
+
"#00ff00",
|
|
23
|
+
"#ffff00",
|
|
24
|
+
"#0000ff",
|
|
25
|
+
"#ff00ff",
|
|
26
|
+
"#00ffff",
|
|
27
|
+
"#ffffff",
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
if (index >= 0 && index < ansi16.length) {
|
|
31
|
+
return ansi16[index];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (index >= 16 && index <= 231) {
|
|
35
|
+
const cube = [0, 95, 135, 175, 215, 255];
|
|
36
|
+
const value = index - 16;
|
|
37
|
+
const r = cube[Math.floor(value / 36)] ?? 0;
|
|
38
|
+
const g = cube[Math.floor((value % 36) / 6)] ?? 0;
|
|
39
|
+
const b = cube[value % 6] ?? 0;
|
|
40
|
+
return rgbToHex(r, g, b);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (index >= 232 && index <= 255) {
|
|
44
|
+
const gray = 8 + (index - 232) * 10;
|
|
45
|
+
return rgbToHex(gray, gray, gray);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function ansiColorToCss(value: string | undefined) {
|
|
52
|
+
if (!value) return "";
|
|
53
|
+
|
|
54
|
+
const trueColorMatch = /\x1b\[38;2;(\d+);(\d+);(\d+)m/.exec(value);
|
|
55
|
+
if (trueColorMatch) {
|
|
56
|
+
return rgbToHex(Number(trueColorMatch[1]), Number(trueColorMatch[2]), Number(trueColorMatch[3]));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const color256Match = /\x1b\[38;5;(\d+)m/.exec(value);
|
|
60
|
+
if (color256Match) {
|
|
61
|
+
return xterm256ToHex(Number(color256Match[1]));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function buildThemePayload(theme?: ThemeLike | null) {
|
|
68
|
+
if (!theme) return null;
|
|
69
|
+
|
|
70
|
+
const colors = {
|
|
71
|
+
accent: ansiColorToCss(theme.getFgAnsi("accent")),
|
|
72
|
+
mdCode: ansiColorToCss(theme.getFgAnsi("mdCode")),
|
|
73
|
+
mdCodeBlock: ansiColorToCss(theme.getFgAnsi("mdCodeBlock")),
|
|
74
|
+
mdCodeBlockBorder: ansiColorToCss(theme.getFgAnsi("mdCodeBlockBorder")),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (!Object.values(colors).some(Boolean)) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
name: theme.name || "",
|
|
83
|
+
colors,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ImageContent, TextContent } from "@mariozechner/pi-ai";
|
|
2
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
|
|
4
|
+
const INLINE_MESSAGE_TYPE = "phone-inline-user-message";
|
|
5
|
+
const INLINE_IMAGE_TOKEN_PATTERN = /⟦img\d+⟧|\{img\d*\}/g;
|
|
6
|
+
|
|
7
|
+
function isInlineImageTokenPrompt(text: string): boolean {
|
|
8
|
+
INLINE_IMAGE_TOKEN_PATTERN.lastIndex = 0;
|
|
9
|
+
return INLINE_IMAGE_TOKEN_PATTERN.test(text);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function buildInlineContent(text: string, images: ImageContent[]) {
|
|
13
|
+
INLINE_IMAGE_TOKEN_PATTERN.lastIndex = 0;
|
|
14
|
+
const matches = [...text.matchAll(INLINE_IMAGE_TOKEN_PATTERN)];
|
|
15
|
+
if (matches.length === 0 || images.length === 0) {
|
|
16
|
+
return { content: text ? [{ type: "text", text } satisfies TextContent] : [], matchedCount: 0 };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const content: (TextContent | ImageContent)[] = [];
|
|
20
|
+
let lastIndex = 0;
|
|
21
|
+
let imageIndex = 0;
|
|
22
|
+
|
|
23
|
+
for (const match of matches) {
|
|
24
|
+
const token = match[0] || "";
|
|
25
|
+
const index = match.index ?? -1;
|
|
26
|
+
if (index < 0) continue;
|
|
27
|
+
if (imageIndex >= images.length) break;
|
|
28
|
+
|
|
29
|
+
const before = text.slice(lastIndex, index);
|
|
30
|
+
if (before) {
|
|
31
|
+
content.push({ type: "text", text: before });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const image = images[imageIndex];
|
|
35
|
+
if (image?.type === "image" && image.data && image.mimeType) {
|
|
36
|
+
content.push({
|
|
37
|
+
type: "image",
|
|
38
|
+
data: image.data,
|
|
39
|
+
mimeType: image.mimeType,
|
|
40
|
+
});
|
|
41
|
+
imageIndex += 1;
|
|
42
|
+
} else {
|
|
43
|
+
content.push({ type: "text", text: token });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
lastIndex = index + token.length;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const after = text.slice(lastIndex);
|
|
50
|
+
if (after) {
|
|
51
|
+
content.push({ type: "text", text: after });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
while (imageIndex < images.length) {
|
|
55
|
+
const image = images[imageIndex];
|
|
56
|
+
if (image?.type === "image" && image.data && image.mimeType) {
|
|
57
|
+
content.push({
|
|
58
|
+
type: "image",
|
|
59
|
+
data: image.data,
|
|
60
|
+
mimeType: image.mimeType,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
imageIndex += 1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { content, matchedCount: Math.min(matches.length, images.length) };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function flattenText(content: (TextContent | ImageContent)[]): string {
|
|
70
|
+
return content
|
|
71
|
+
.filter((part): part is TextContent => part.type === "text")
|
|
72
|
+
.map((part) => part.text)
|
|
73
|
+
.join("");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export default function registerPhoneChildExtension(pi: ExtensionAPI) {
|
|
77
|
+
pi.on("input", async (event, ctx) => {
|
|
78
|
+
if (event.source !== "rpc" || !Array.isArray(event.images) || event.images.length === 0) {
|
|
79
|
+
return { action: "continue" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!isInlineImageTokenPrompt(event.text || "")) {
|
|
83
|
+
return { action: "continue" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const { content, matchedCount } = buildInlineContent(event.text || "", event.images as ImageContent[]);
|
|
87
|
+
if (matchedCount === 0) {
|
|
88
|
+
return { action: "continue" };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const trimmed = (event.text || "").trimStart();
|
|
92
|
+
const looksLikeSlashCommand = trimmed.startsWith("/");
|
|
93
|
+
if (!ctx.isIdle() || looksLikeSlashCommand) {
|
|
94
|
+
return {
|
|
95
|
+
action: "transform",
|
|
96
|
+
text: flattenText(content),
|
|
97
|
+
images: content.filter((part): part is ImageContent => part.type === "image"),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
pi.sendMessage(
|
|
102
|
+
{
|
|
103
|
+
customType: INLINE_MESSAGE_TYPE,
|
|
104
|
+
content,
|
|
105
|
+
display: true,
|
|
106
|
+
},
|
|
107
|
+
{ triggerTurn: true },
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return { action: "handled" };
|
|
111
|
+
});
|
|
112
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { PhoneServerRuntime } from "./phone-server-runtime";
|
|
3
|
+
|
|
4
|
+
export default function registerPhoneExtension(pi: ExtensionAPI) {
|
|
5
|
+
if (process.env.DM_PHONE_CHILD === "1" || process.env.PI_PHONE_CHILD === "1") {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const runtime = new PhoneServerRuntime(pi);
|
|
10
|
+
|
|
11
|
+
pi.registerCommand("phone-start", {
|
|
12
|
+
description: "Start the phone web UI. Usage: /phone-start [port] [token] [--cwd path] [--host 127.0.0.1] [--idle-mins 20]",
|
|
13
|
+
handler: async (args, ctx) => {
|
|
14
|
+
await runtime.handlePhoneStart(args, ctx);
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
pi.registerCommand("phone-stop", {
|
|
19
|
+
description: "Stop the phone web UI server and remove the matching Tailscale Serve route",
|
|
20
|
+
handler: async (_args, ctx) => {
|
|
21
|
+
await runtime.handlePhoneStop(ctx);
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
pi.registerCommand("phone-status", {
|
|
26
|
+
description: "Show phone server and Tailscale Serve status",
|
|
27
|
+
handler: async (_args, ctx) => {
|
|
28
|
+
await runtime.handlePhoneStatus(ctx);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
pi.registerCommand("phone-token", {
|
|
33
|
+
description: "Show the current phone UI token",
|
|
34
|
+
handler: async (_args, ctx) => {
|
|
35
|
+
runtime.handlePhoneToken(ctx);
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
pi.on("input", async (event, ctx) => {
|
|
40
|
+
return runtime.handleInput(event, ctx);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
44
|
+
await runtime.handleSessionStart(ctx);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
48
|
+
await runtime.handleSessionSwitch(ctx);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
pi.on("session_fork", async (_event, ctx) => {
|
|
52
|
+
await runtime.handleSessionSwitch(ctx);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
pi.on("session_tree", async (_event, ctx) => {
|
|
56
|
+
await runtime.handleSessionSwitch(ctx);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
pi.on("session_before_compact", async (_event, ctx) => {
|
|
60
|
+
runtime.handleParentCompactionStart(ctx);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
pi.on("session_compact", async (_event, ctx) => {
|
|
64
|
+
runtime.handleParentCompactionEnd(ctx);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
68
|
+
await runtime.handleSessionSwitch(ctx);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
pi.on("agent_start", async (_event, ctx) => {
|
|
72
|
+
runtime.handleParentAgentStart(ctx);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
pi.on("agent_end", async (_event, ctx) => {
|
|
76
|
+
runtime.handleParentAgentEnd(ctx);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
pi.on("message_start", async (event, ctx) => {
|
|
80
|
+
runtime.handleParentMessageStart(event, ctx);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
pi.on("message_update", async (event, ctx) => {
|
|
84
|
+
runtime.handleParentMessageUpdate(event, ctx);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
pi.on("message_end", async (event, ctx) => {
|
|
88
|
+
runtime.handleParentMessageEnd(event, ctx);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
pi.on("tool_execution_start", async (event, ctx) => {
|
|
92
|
+
runtime.handleParentToolExecutionStart(event, ctx);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
pi.on("tool_execution_update", async (event, ctx) => {
|
|
96
|
+
runtime.handleParentToolExecutionUpdate(event, ctx);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
pi.on("tool_execution_end", async (event, ctx) => {
|
|
100
|
+
runtime.handleParentToolExecutionEnd(event, ctx);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
104
|
+
await runtime.handleSessionShutdown(ctx);
|
|
105
|
+
});
|
|
106
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { WebSocket } from "ws";
|
|
2
|
+
|
|
3
|
+
export type PhoneConfig = {
|
|
4
|
+
host: string;
|
|
5
|
+
port: number;
|
|
6
|
+
token: string;
|
|
7
|
+
cwd: string;
|
|
8
|
+
idleTimeoutMs: number;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ParsedPhoneArgs = {
|
|
12
|
+
config: PhoneConfig;
|
|
13
|
+
tokenSpecified: boolean;
|
|
14
|
+
idleSpecified: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type PersistedPhoneRuntime = {
|
|
18
|
+
pid: number;
|
|
19
|
+
host: string;
|
|
20
|
+
port: number;
|
|
21
|
+
controlToken: string;
|
|
22
|
+
startedAt: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type PendingClientResponse = {
|
|
26
|
+
ws: WebSocket;
|
|
27
|
+
responseCommand?: string;
|
|
28
|
+
responseData?: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type UsageWindow = {
|
|
32
|
+
used_percent?: number | null;
|
|
33
|
+
reset_after_seconds?: number | null;
|
|
34
|
+
reset_at?: number | null;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
export type RateLimitBucket = {
|
|
38
|
+
allowed?: boolean;
|
|
39
|
+
limit_reached?: boolean;
|
|
40
|
+
primary_window?: UsageWindow | null;
|
|
41
|
+
secondary_window?: UsageWindow | null;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type CodexUsageResponse = {
|
|
45
|
+
rate_limit?: RateLimitBucket | null;
|
|
46
|
+
additional_rate_limits?: Record<string, unknown> | unknown[] | null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export type PhoneQuotaWindow = {
|
|
50
|
+
label: "5h" | "7d";
|
|
51
|
+
leftPercent: number;
|
|
52
|
+
usedPercent: number;
|
|
53
|
+
resetAfterSeconds: number | null;
|
|
54
|
+
text: string;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export type PhoneQuotaResponse = {
|
|
58
|
+
visible: boolean;
|
|
59
|
+
limited: boolean;
|
|
60
|
+
primaryWindow: PhoneQuotaWindow | null;
|
|
61
|
+
secondaryWindow: PhoneQuotaWindow | null;
|
|
62
|
+
error?: string;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export type PhonePathSuggestionMode = "mention" | "cd";
|
|
66
|
+
|
|
67
|
+
export type PhonePathSuggestion = {
|
|
68
|
+
value: string;
|
|
69
|
+
label: string;
|
|
70
|
+
description?: string;
|
|
71
|
+
isDirectory: boolean;
|
|
72
|
+
kind: "path" | "previous";
|
|
73
|
+
};
|