@agimon-ai/doompi-read 0.0.1-alpha.2 → 0.0.1-alpha.21

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/README.md CHANGED
@@ -15,7 +15,7 @@ people and preserves syntax highlighting. Image reads retain Pi's native attachm
15
15
  ## Requirements
16
16
 
17
17
  - Node.js 22.19.0 or newer
18
- - Pi 0.84.2 and Pi TUI 0.84.2
18
+ - Pi 0.84.4 and Pi TUI 0.84.4
19
19
 
20
20
  ## Install
21
21
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agimon-ai/doompi-read",
3
- "version": "0.0.1-alpha.2",
3
+ "version": "0.0.1-alpha.21",
4
4
  "description": "Snapshot-bound hashline read tool for Pi and DoomPi.",
5
5
  "keywords": [
6
6
  "ai",
@@ -14,10 +14,20 @@
14
14
  "read"
15
15
  ],
16
16
  "homepage": "https://agimon.ai",
17
+ "bugs": {
18
+ "url": "https://github.com/AgiFlow/doompi/issues"
19
+ },
17
20
  "license": "MIT",
18
21
  "author": "Vuong Ngo",
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "git+https://github.com/AgiFlow/doompi.git",
25
+ "directory": "packages/default/doompi-read"
26
+ },
19
27
  "files": [
20
- "dist"
28
+ "dist",
29
+ "src/web",
30
+ "src/exports/webClient.ts"
21
31
  ],
22
32
  "type": "module",
23
33
  "main": "./dist/index.cjs",
@@ -41,23 +51,29 @@
41
51
  },
42
52
  "dependencies": {
43
53
  "@deepseek-ai/cordis": "4.0.1",
44
- "typebox": "1.3.16",
45
- "@agimon-ai/doompi-hashline": "0.0.1-alpha.2",
46
- "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.30",
47
- "@agimon-ai/doompi-ui": "0.0.1-alpha.30"
54
+ "typebox": "1.3.19",
55
+ "@agimon-ai/doompi-extension-contracts": "0.0.1-alpha.48",
56
+ "@agimon-ai/doompi-hashline": "0.0.1-alpha.20",
57
+ "@agimon-ai/doompi-ui": "0.0.1-alpha.49",
58
+ "@agimon-ai/doompi-web-contracts": "0.0.1-alpha.12",
59
+ "@agimon-ai/doompi-web-components": "0.0.1-alpha.9"
48
60
  },
49
61
  "devDependencies": {
50
- "@earendil-works/pi-coding-agent": "0.84.2",
51
- "@earendil-works/pi-tui": "0.84.2",
52
- "@types/node": "26.2.0",
62
+ "@earendil-works/pi-coding-agent": "0.84.4",
63
+ "@earendil-works/pi-tui": "0.84.4",
64
+ "@tanstack/react-store": "0.11.1",
65
+ "@tanstack/store": "0.11.1",
66
+ "@types/node": "26.4.0",
67
+ "@types/react": "19.2.18",
53
68
  "@vitest/coverage-v8": "4.1.11",
69
+ "react": "19.2.8",
54
70
  "tsdown": "0.22.14",
55
71
  "typescript": "7.0.2",
56
72
  "vitest": "4.1.11"
57
73
  },
58
74
  "peerDependencies": {
59
- "@earendil-works/pi-coding-agent": "0.84.2",
60
- "@earendil-works/pi-tui": "0.84.2"
75
+ "@earendil-works/pi-coding-agent": "0.84.4",
76
+ "@earendil-works/pi-tui": "0.84.4"
61
77
  },
62
78
  "peerDependenciesMeta": {
63
79
  "@earendil-works/pi-coding-agent": {
@@ -70,6 +86,11 @@
70
86
  "engines": {
71
87
  "node": ">=22.19.0"
72
88
  },
89
+ "doompiWeb": {
90
+ "pluginId": "read",
91
+ "channels": [],
92
+ "client": "./src/exports/webClient.ts"
93
+ },
73
94
  "pi": {
74
95
  "extensions": [
75
96
  "./dist/extensions/pi.mjs"
@@ -78,7 +99,7 @@
78
99
  "scripts": {
79
100
  "build": "tsdown",
80
101
  "test": "vitest --run",
81
- "typecheck": "tsc --noEmit",
102
+ "typecheck": "tsc --noEmit && tsc --noEmit -p src/web/tsconfig.json",
82
103
  "lint": "oxlint . && oxfmt . --check",
83
104
  "fixcode": "oxlint . --fix && oxfmt ."
84
105
  }
@@ -0,0 +1 @@
1
+ export { webPlugin } from '../web/index.ts';
@@ -0,0 +1,116 @@
1
+ import {
2
+ HashlineLines,
3
+ hashlineBody,
4
+ MessageItem,
5
+ MessageItemBody,
6
+ MessageItemHeader,
7
+ MessageItemStatus,
8
+ resultTextLines,
9
+ ToolPathLink,
10
+ toolTone,
11
+ } from '@agimon-ai/doompi-web-components';
12
+ import type { ToolMessageRenderProps } from '@agimon-ai/doompi-web-contracts';
13
+ import { readCallView } from './readToolView.ts';
14
+
15
+ const IMAGE_MIME_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
16
+
17
+ function resultImages(content: readonly unknown[]): Array<{ data: string; mimeType: string }> {
18
+ return content.flatMap((block) => {
19
+ if (typeof block !== 'object' || block === null || Array.isArray(block)) return [];
20
+ const value = block as Record<string, unknown>;
21
+ return value.type === 'image' &&
22
+ typeof value.data === 'string' &&
23
+ typeof value.mimeType === 'string' &&
24
+ IMAGE_MIME_TYPES.has(value.mimeType)
25
+ ? [{ data: value.data, mimeType: value.mimeType }]
26
+ : [];
27
+ });
28
+ }
29
+ /**
30
+ * The read tool's timeline item, the web half of its renderCall and
31
+ * renderResult: the call summary in the header and the anchored lines with
32
+ * a line-number gutter in the body, collapsed to the TUI's budget until the
33
+ * item expands. A failure shows the tool's message in red instead.
34
+ */
35
+ export function ReadToolMessage({
36
+ args,
37
+ result,
38
+ output,
39
+ running,
40
+ isError,
41
+ fileTabFor,
42
+ openTransientTab,
43
+ }: ToolMessageRenderProps) {
44
+ const view = readCallView(args);
45
+ const images = resultImages(result?.content ?? []);
46
+ // A read is the one call whose file the session usually has not changed, so
47
+ // this is the link that opens it read-only rather than in its history.
48
+ const tab = fileTabFor(view.path);
49
+ const collapsed = result === null || isError ? null : hashlineBody(result, output, 'read', false);
50
+ return (
51
+ <MessageItem tone={toolTone({ running, isError })} expandable={collapsed !== null && collapsed.hidden > 0}>
52
+ {({ expanded }) => {
53
+ const body =
54
+ result === null || isError ? null : expanded ? hashlineBody(result, output, 'read', true) : collapsed;
55
+ return (
56
+ <>
57
+ <MessageItemHeader title="read">
58
+ <span data-testid="tool-call-read" className="flex min-w-0 flex-1 items-center gap-2">
59
+ <ToolPathLink
60
+ path={view.path}
61
+ {...(tab === undefined ? {} : { onOpen: () => openTransientTab(tab) })}
62
+ />
63
+ {view.details.length > 0 ? (
64
+ <span className="shrink-0 text-doom-faint">· {view.details.join(' · ')}</span>
65
+ ) : null}
66
+ </span>
67
+ </MessageItemHeader>
68
+ {isError ? (
69
+ <MessageItemBody data-testid="tool-result-read" className="flex flex-col">
70
+ <MessageItemStatus tone="error">failed</MessageItemStatus>
71
+ {resultTextLines(result, output).map((line, index) => (
72
+ <span key={`${String(index)}-${line}`} className="whitespace-pre-wrap break-words text-doom-red">
73
+ {line}
74
+ </span>
75
+ ))}
76
+ </MessageItemBody>
77
+ ) : body === null ? (
78
+ running ? (
79
+ <MessageItemBody data-testid="tool-result-read">
80
+ <MessageItemStatus tone="running">running</MessageItemStatus>
81
+ </MessageItemBody>
82
+ ) : null
83
+ ) : body.shown.length === 0 && body.notice === undefined && images.length === 0 ? null : (
84
+ <MessageItemBody data-testid="tool-result-read" className="flex flex-col gap-1">
85
+ {/* A read body is one file with no heading of its own, so the
86
+ call's path is what names the grammar for it. */}
87
+ <HashlineLines lines={body.shown} gutter={body.gutter} path={view.path} />
88
+ {images.map((image, index) => {
89
+ const url = `data:${image.mimeType};base64,${image.data}`;
90
+ return (
91
+ <a
92
+ key={`${image.mimeType}:${String(index)}`}
93
+ href={url}
94
+ target="_blank"
95
+ rel="noreferrer"
96
+ className="self-start"
97
+ >
98
+ <img
99
+ src={url}
100
+ alt={`Read result ${String(index + 1)}`}
101
+ data-testid="tool-result-read-image"
102
+ className="h-auto max-h-96 max-w-full rounded-md border border-doom-border-soft object-contain"
103
+ />
104
+ </a>
105
+ );
106
+ })}
107
+ {body.hidden > 0 ? <MessageItemStatus expands>{body.hidden} more lines</MessageItemStatus> : null}
108
+ {body.notice !== undefined ? <span className="text-doom-faint">{body.notice}</span> : null}
109
+ </MessageItemBody>
110
+ )}
111
+ </>
112
+ );
113
+ }}
114
+ </MessageItem>
115
+ );
116
+ }
@@ -0,0 +1,11 @@
1
+ import { defineWebPlugin } from '@agimon-ai/doompi-web-contracts';
2
+ import { ReadToolMessage } from './ReadToolMessage.tsx';
3
+
4
+ /**
5
+ * This package's cockpit presence: the read tool's timeline card, the web
6
+ * half of the TUI's renderCall and renderResult for hashline reads.
7
+ */
8
+ export const webPlugin = defineWebPlugin({
9
+ id: 'read',
10
+ toolRenderers: [{ tools: ['read'], message: ReadToolMessage }],
11
+ });
@@ -0,0 +1,24 @@
1
+ import { compactDetails } from '@agimon-ai/doompi-web-components';
2
+
3
+ export interface ReadCallView {
4
+ readonly path: string;
5
+ /** `from N` and `N lines`, as the TUI's call heading lists them. */
6
+ readonly details: readonly string[];
7
+ }
8
+
9
+ function optionalNumber(value: unknown): number | undefined {
10
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
11
+ }
12
+
13
+ /** What the read call heading shows: the path, then the offset and limit when given. */
14
+ export function readCallView(args: Readonly<Record<string, unknown>>): ReadCallView {
15
+ const offset = optionalNumber(args.offset);
16
+ const limit = optionalNumber(args.limit);
17
+ return {
18
+ path: typeof args.path === 'string' ? args.path : '',
19
+ details: compactDetails([
20
+ offset === undefined ? undefined : `from ${offset}`,
21
+ limit === undefined ? undefined : `${limit} lines`,
22
+ ]),
23
+ };
24
+ }
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2023", "DOM", "DOM.Iterable"],
5
+ "module": "ESNext",
6
+ "moduleResolution": "Bundler",
7
+ "jsx": "react-jsx",
8
+ "strict": true,
9
+ "noImplicitAny": true,
10
+ "noUnusedLocals": true,
11
+ "noUnusedParameters": true,
12
+ "noImplicitReturns": true,
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noEmit": true,
15
+ "resolveJsonModule": true,
16
+ "esModuleInterop": true,
17
+ "skipLibCheck": true,
18
+ "allowImportingTsExtensions": true,
19
+ "forceConsistentCasingInFileNames": true,
20
+ "types": ["node"]
21
+ },
22
+ "include": [".", "../types", "../exports/webClient.ts"]
23
+ }