@narumitw/pi-inspect 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,175 @@
1
+ .tool-inspector {
2
+ container-type: inline-size;
3
+ font:
4
+ 11px / 1.5 Consolas,
5
+ "SFMono-Regular",
6
+ monospace;
7
+ min-width: 0;
8
+ }
9
+ .tool-browser {
10
+ display: grid;
11
+ grid-template-columns: minmax(130px, 30%) minmax(0, 1fr);
12
+ gap: 10px;
13
+ }
14
+ .tool-navigation {
15
+ min-width: 0;
16
+ border-right: 1px solid var(--panel-border);
17
+ padding-right: 8px;
18
+ }
19
+ .tool-navigation input {
20
+ width: 100%;
21
+ min-width: 0;
22
+ padding: 5px 7px;
23
+ border: 1px solid var(--panel-border);
24
+ border-radius: 4px;
25
+ background: var(--panel-bg);
26
+ color: inherit;
27
+ }
28
+ .tool-list {
29
+ display: grid;
30
+ gap: 3px;
31
+ max-height: 320px;
32
+ overflow-y: auto;
33
+ margin-top: 6px;
34
+ }
35
+ .tool-list button {
36
+ text-align: left;
37
+ border: 0;
38
+ border-left: 2px solid transparent;
39
+ border-radius: 3px;
40
+ background: transparent;
41
+ color: inherit;
42
+ padding: 5px 7px;
43
+ min-width: 0;
44
+ }
45
+ .tool-list button:hover {
46
+ background: var(--surface-hover);
47
+ }
48
+ .tool-list button[aria-pressed="true"] {
49
+ background: var(--selected-bg);
50
+ border-left-color: var(--accent-9);
51
+ }
52
+ .tool-list strong {
53
+ display: -webkit-box;
54
+ -webkit-line-clamp: 2;
55
+ -webkit-box-orient: vertical;
56
+ overflow: hidden;
57
+ overflow-wrap: anywhere;
58
+ }
59
+ .tool-list span {
60
+ display: block;
61
+ white-space: nowrap;
62
+ overflow: hidden;
63
+ text-overflow: ellipsis;
64
+ color: var(--secondary);
65
+ }
66
+ .tool-documentation {
67
+ min-width: 0;
68
+ overflow-wrap: anywhere;
69
+ }
70
+ .tool-heading {
71
+ display: flex;
72
+ align-items: start;
73
+ justify-content: space-between;
74
+ gap: 8px;
75
+ }
76
+ .tool-inspector .copy-button {
77
+ margin: 0;
78
+ flex-shrink: 0;
79
+ }
80
+ .tool-heading h3 {
81
+ min-width: 0;
82
+ margin: 0 0 6px;
83
+ font-size: 14px;
84
+ }
85
+ .tool-documentation h4 {
86
+ margin: 12px 0 4px;
87
+ font-size: 11px;
88
+ }
89
+ .tool-prose {
90
+ white-space: pre-wrap;
91
+ overflow-wrap: anywhere;
92
+ margin: 8px 0;
93
+ }
94
+ .tool-muted,
95
+ .schema-annotation {
96
+ color: var(--secondary);
97
+ }
98
+ .tool-documentation pre {
99
+ max-width: 100%;
100
+ overflow-x: auto;
101
+ white-space: pre;
102
+ padding: 7px;
103
+ border: 1px solid var(--panel-border);
104
+ border-radius: 3px;
105
+ background: var(--code-bg);
106
+ font: inherit;
107
+ }
108
+ .schema-scroll {
109
+ max-width: 100%;
110
+ overflow-x: auto;
111
+ }
112
+ .parameter-schema {
113
+ width: 100%;
114
+ min-width: 400px;
115
+ border-collapse: collapse;
116
+ table-layout: fixed;
117
+ font-size: 10px;
118
+ }
119
+ .parameter-schema caption {
120
+ text-align: left;
121
+ font-weight: bold;
122
+ padding: 8px 0 4px;
123
+ }
124
+ .parameter-schema th,
125
+ .parameter-schema td {
126
+ text-align: left;
127
+ vertical-align: top;
128
+ padding: 6px 5px;
129
+ border-bottom: 1px solid var(--panel-border);
130
+ overflow-wrap: anywhere;
131
+ }
132
+ .parameter-schema thead {
133
+ color: var(--secondary);
134
+ background: var(--code-bg);
135
+ }
136
+ .parameter-schema th:nth-child(1) {
137
+ width: 25%;
138
+ }
139
+ .parameter-schema th:nth-child(2) {
140
+ width: 18%;
141
+ }
142
+ .parameter-schema th:nth-child(3) {
143
+ width: 16%;
144
+ }
145
+ .parameter-schema code {
146
+ font: inherit;
147
+ }
148
+ .schema-type {
149
+ color: var(--accent-11);
150
+ }
151
+ .schema-toggle {
152
+ text-align: left;
153
+ padding: 0;
154
+ border: 0;
155
+ color: inherit;
156
+ background: transparent;
157
+ font-weight: inherit;
158
+ overflow-wrap: anywhere;
159
+ }
160
+ .parameter-schema .tool-prose {
161
+ margin: 0 0 3px;
162
+ }
163
+ @container (max-width: 540px) {
164
+ .tool-browser {
165
+ grid-template-columns: minmax(0, 1fr);
166
+ }
167
+ .tool-navigation {
168
+ border-right: 0;
169
+ border-bottom: 1px solid var(--panel-border);
170
+ padding: 0 0 8px;
171
+ }
172
+ .tool-list {
173
+ max-height: 145px;
174
+ }
175
+ }
@@ -0,0 +1,210 @@
1
+ import { Fragment, useMemo, useState } from "react";
2
+ import type { Capture, Json } from "../model.js";
3
+ import { Copy, Data } from "./components.js";
4
+ import { output } from "./format.js";
5
+ import { parseSchema, type SchemaNode, type ToolDoc, toolDocuments } from "./tool-schema.js";
6
+
7
+ function DocumentationText({ value }: { value: string }) {
8
+ const parts = useMemo(() => {
9
+ const parts: { id: string; text: string; code: boolean }[] = [];
10
+ let offset = 0;
11
+ for (const match of value.matchAll(/^```[^\n]*\n([\s\S]*?)^```[ \t]*(?:\n|$)/gm)) {
12
+ parts.push({ id: `text-${offset}`, text: value.slice(offset, match.index), code: false });
13
+ parts.push({ id: `code-${match.index}`, text: match[1] ?? "", code: true });
14
+ offset = match.index + match[0].length;
15
+ }
16
+ parts.push({ id: `text-${offset}`, text: value.slice(offset), code: false });
17
+ return parts;
18
+ }, [value]);
19
+ return parts.map((part) =>
20
+ part.code ? (
21
+ <pre key={part.id}>{part.text}</pre>
22
+ ) : (
23
+ <div key={part.id} className="tool-prose">
24
+ {part.text}
25
+ </div>
26
+ ),
27
+ );
28
+ }
29
+
30
+ function SchemaRows({ node, depth = 0 }: { node: SchemaNode; depth?: number }) {
31
+ const [open, setOpen] = useState(false);
32
+ return (
33
+ <Fragment>
34
+ <tr>
35
+ <th scope="row" style={{ paddingLeft: `${8 + Math.min(depth, 8) * 12}px` }}>
36
+ {node.children.length ? (
37
+ <button type="button" className="schema-toggle" aria-expanded={open} onClick={() => setOpen(!open)}>
38
+ <span aria-hidden="true">{open ? "▾" : "▸"}</span> {node.name}
39
+ </button>
40
+ ) : (
41
+ node.name
42
+ )}
43
+ </th>
44
+ <td>
45
+ <code className="schema-type">{node.type}</code>
46
+ </td>
47
+ <td
48
+ title={
49
+ node.required === "unknown"
50
+ ? "Other schema constraints or documentation limits may require this field; inspect the schema branches or Raw JSON."
51
+ : undefined
52
+ }
53
+ >
54
+ {node.required === undefined ? "—" : node.required === "unknown" ? "Unknown" : node.required ? "Yes" : "No"}
55
+ </td>
56
+ <td>
57
+ {node.description && <div className="tool-prose">{node.description}</div>}
58
+ {node.annotations.map(([key, value]) => (
59
+ <div className="schema-annotation" key={key}>
60
+ <span>{key}: </span>
61
+ <code>{JSON.stringify(value)}</code>
62
+ </div>
63
+ ))}
64
+ {node.limited && (
65
+ <p role="status">Documentation limit reached; inspect Raw JSON for the complete captured schema.</p>
66
+ )}
67
+ </td>
68
+ </tr>
69
+ {open && node.children.map((child) => <SchemaRows key={child.id} node={child} depth={depth + 1} />)}
70
+ </Fragment>
71
+ );
72
+ }
73
+ export function ParameterSchema({ value }: { value: Json }) {
74
+ const root = useMemo(() => parseSchema(value), [value]);
75
+ return (
76
+ <div className="schema-scroll">
77
+ <table className="parameter-schema">
78
+ <caption>Input parameters</caption>
79
+ <thead>
80
+ <tr>
81
+ <th scope="col">Parameter</th>
82
+ <th scope="col">Type</th>
83
+ <th scope="col">Required</th>
84
+ <th scope="col">Description / constraints</th>
85
+ </tr>
86
+ </thead>
87
+ <tbody>
88
+ <SchemaRows node={{ ...root, children: [] }} />
89
+ {root.children.map((node) => (
90
+ <SchemaRows key={node.id} node={node} />
91
+ ))}
92
+ </tbody>
93
+ </table>
94
+ </div>
95
+ );
96
+ }
97
+ function ToolDetails({ tool, scope, truncated }: { tool: ToolDoc; scope: string; truncated: boolean }) {
98
+ const [mode, setMode] = useState("documentation");
99
+ return (
100
+ <section className="tool-documentation" aria-label="Selected tool documentation">
101
+ <div className="tool-heading">
102
+ <h3>{tool.name}</h3>
103
+ <Copy key={JSON.stringify(tool.raw)} value={JSON.stringify(tool.raw, null, 2)} />
104
+ </div>
105
+ <div className="view-switch">
106
+ {["documentation", "raw"].map((value) => (
107
+ <button type="button" key={value} aria-pressed={mode === value} onClick={() => setMode(value)}>
108
+ {value === "raw" ? "Raw JSON" : "Documentation"}
109
+ </button>
110
+ ))}
111
+ </div>
112
+ {mode === "raw" ? (
113
+ <Data
114
+ label="Original captured tool declaration"
115
+ data={{ value: tool.raw, truncated }}
116
+ scope={`${scope}-tool-${tool.id}`}
117
+ />
118
+ ) : (
119
+ <>
120
+ {tool.description ? (
121
+ <DocumentationText value={tool.description} />
122
+ ) : (
123
+ <p className="tool-muted">No description supplied.</p>
124
+ )}
125
+ {tool.parameters === undefined ? (
126
+ <p className="tool-muted">Input schema unspecified.</p>
127
+ ) : (
128
+ <ParameterSchema value={tool.parameters} />
129
+ )}
130
+ {tool.returns !== undefined && (
131
+ <section>
132
+ <h4>Returns</h4>
133
+ <p className="tool-muted">Declared or documented return information; not an inferred output schema.</p>
134
+ <pre>{output(tool.returns)}</pre>
135
+ </section>
136
+ )}
137
+ {tool.codemode && (
138
+ <section>
139
+ <h4>Codemode</h4>
140
+ <DocumentationText value={tool.codemode} />
141
+ </section>
142
+ )}
143
+ {tool.examples.length > 0 && (
144
+ <section>
145
+ <h4>Declared examples</h4>
146
+ {tool.examples.map((example) => (
147
+ <div key={example.id}>
148
+ <Copy key={JSON.stringify(example.value)} value={JSON.stringify(example.value, null, 2)} />
149
+ <pre>{JSON.stringify(example.value, null, 2)}</pre>
150
+ </div>
151
+ ))}
152
+ </section>
153
+ )}
154
+ </>
155
+ )}
156
+ </section>
157
+ );
158
+ }
159
+ export function ToolInspector({ data, scope }: { data?: Capture; scope: string }) {
160
+ const tools = useMemo(() => toolDocuments(data?.value), [data?.value]);
161
+ const [query, setQuery] = useState("");
162
+ const [selected, setSelected] = useState<number>();
163
+ const filtered = useMemo(() => {
164
+ const search = query.trim().toLocaleLowerCase();
165
+ return tools.filter((tool) =>
166
+ `${tool.name}\n${tool.description}\n${tool.codemode}`.toLocaleLowerCase().includes(search),
167
+ );
168
+ }, [tools, query]);
169
+ const tool = filtered.find((tool) => tool.id === selected) ?? filtered[0];
170
+ if (!data || !Array.isArray(data.value))
171
+ return <Data label="Historical declared tools · preview branch" data={data} scope={scope} />;
172
+ return (
173
+ <section className="tool-inspector" aria-label="Historical declared tools">
174
+ <p className="tool-muted">Historical declared tools · preview branch · {tools.length} tools</p>
175
+ {data.truncated && (
176
+ <p role="status">
177
+ Captured declarations are truncated. Documentation and Raw JSON show only the available display copy.
178
+ </p>
179
+ )}
180
+ <div className="tool-browser">
181
+ <nav className="tool-navigation" aria-label="Declared tools">
182
+ <input
183
+ type="search"
184
+ aria-label="Search tools"
185
+ placeholder="Search tools…"
186
+ value={query}
187
+ onChange={(event) => setQuery(event.target.value)}
188
+ />
189
+ <div className="tool-list">
190
+ {filtered.map((item) => (
191
+ <button
192
+ type="button"
193
+ key={item.id}
194
+ aria-pressed={tool?.id === item.id}
195
+ onClick={() => setSelected(item.id)}
196
+ >
197
+ <strong>{item.name}</strong>
198
+ <span>{item.description.split("\n")[0] || "No description supplied"}</span>
199
+ </button>
200
+ ))}
201
+ </div>
202
+ {!filtered.length && (
203
+ <p className="tool-muted">{tools.length ? "No matching tools." : "No declared tools."}</p>
204
+ )}
205
+ </nav>
206
+ {tool && <ToolDetails key={`${scope}:${tool.id}`} tool={tool} scope={scope} truncated={data.truncated} />}
207
+ </div>
208
+ </section>
209
+ );
210
+ }
@@ -0,0 +1,217 @@
1
+ import type { Json } from "../model.js";
2
+ import { record } from "./format.js";
3
+
4
+ export interface SchemaNode {
5
+ id: string;
6
+ name: string;
7
+ type: string;
8
+ required?: boolean | "unknown";
9
+ description: string;
10
+ annotations: [string, Json][];
11
+ children: SchemaNode[];
12
+ limited: boolean;
13
+ }
14
+ const structural = new Set([
15
+ "type",
16
+ "description",
17
+ "properties",
18
+ "required",
19
+ "items",
20
+ "prefixItems",
21
+ "additionalProperties",
22
+ "patternProperties",
23
+ "anyOf",
24
+ "oneOf",
25
+ "allOf",
26
+ "not",
27
+ "if",
28
+ "then",
29
+ "else",
30
+ "$defs",
31
+ "definitions",
32
+ ]);
33
+ function schemaType(value: Json): string {
34
+ if (value === true) return "any";
35
+ if (value === false) return "never";
36
+ const schema = record(value);
37
+ if (!schema) return "unspecified";
38
+ if (typeof schema.type === "string") return schema.type;
39
+ if (Array.isArray(schema.type)) return schema.type.filter((type) => typeof type === "string").join(" | ");
40
+ if (schema.$ref) return "reference";
41
+ if (schema.anyOf || schema.oneOf || schema.allOf) return "composition";
42
+ return "unspecified";
43
+ }
44
+
45
+ interface RequiredProperties {
46
+ names: Set<string>;
47
+ unresolved: boolean;
48
+ }
49
+ // Resolve only unconditional required/allOf declarations at the same instance.
50
+ // References and other assertions can require fields; their absence from a local
51
+ // required array is not sufficient evidence to label a parameter optional.
52
+ function requiredProperties(value: Json, depth = 0, budget = { remaining: 2000 }): RequiredProperties {
53
+ const schema = record(value);
54
+ const names = new Set(
55
+ Array.isArray(schema?.required) ? schema.required.filter((name): name is string => typeof name === "string") : [],
56
+ );
57
+ if (--budget.remaining < 0 || depth >= 32) return { names, unresolved: true };
58
+ if (!schema) return { names, unresolved: value !== true };
59
+ let unresolved = [
60
+ "$ref",
61
+ "$dynamicRef",
62
+ "$recursiveRef",
63
+ "anyOf",
64
+ "oneOf",
65
+ "not",
66
+ "if",
67
+ "then",
68
+ "else",
69
+ "dependentRequired",
70
+ "dependentSchemas",
71
+ "dependencies",
72
+ "minProperties",
73
+ "const",
74
+ "enum",
75
+ ].some((keyword) => schema[keyword] !== undefined);
76
+ if (schema.required !== undefined && !Array.isArray(schema.required)) unresolved = true;
77
+ if (schema.allOf !== undefined) {
78
+ if (!Array.isArray(schema.allOf)) unresolved = true;
79
+ else
80
+ for (const part of schema.allOf) {
81
+ const child = requiredProperties(part, depth + 1, budget);
82
+ for (const name of child.names) names.add(name);
83
+ unresolved ||= child.unresolved;
84
+ if (budget.remaining <= 0) {
85
+ unresolved = true;
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ return { names, unresolved };
91
+ }
92
+
93
+ // Limit documentation work, never the stored declaration. Keep references and
94
+ // composition branches explicit rather than rewriting or flattening the schema.
95
+ export function parseSchema(value: Json): SchemaNode {
96
+ let remaining = 2000;
97
+ function parse(
98
+ value: Json,
99
+ name: string,
100
+ required?: SchemaNode["required"],
101
+ depth = 0,
102
+ inherited?: RequiredProperties,
103
+ ): SchemaNode {
104
+ const schema = record(value);
105
+ const node: SchemaNode = {
106
+ id: `${remaining}:${name}`,
107
+ name,
108
+ type: schemaType(value),
109
+ required,
110
+ description: typeof schema?.description === "string" ? schema.description : "",
111
+ annotations: [],
112
+ children: [],
113
+ limited: false,
114
+ };
115
+ if (--remaining < 0 || depth >= 32) {
116
+ node.limited = true;
117
+ return node;
118
+ }
119
+ if (!schema) {
120
+ if (typeof value !== "boolean") node.annotations.push(["Unrecognized schema", value]);
121
+ return node;
122
+ }
123
+ node.annotations = Object.entries(schema).filter(([key]) => !structural.has(key));
124
+ const requirements = requiredProperties(value);
125
+ if (inherited) {
126
+ for (const name of inherited.names) requirements.names.add(name);
127
+ requirements.unresolved ||= inherited.unresolved;
128
+ }
129
+ function child(value: Json, label: string, required?: SchemaNode["required"], inherited?: RequiredProperties) {
130
+ if (remaining <= 0) {
131
+ node.limited = true;
132
+ return;
133
+ }
134
+ node.children.push(parse(value, label, required, depth + 1, inherited));
135
+ }
136
+ for (const [key, property] of Object.entries(record(schema.properties) ?? {})) {
137
+ child(property, key, requirements.names.has(key) ? true : requirements.unresolved ? "unknown" : false);
138
+ }
139
+ if (schema.required !== undefined) node.annotations.push(["required", schema.required]);
140
+ for (const keyword of ["patternProperties", "$defs", "definitions"]) {
141
+ for (const [key, property] of Object.entries(record(schema[keyword]) ?? {}))
142
+ child(property, `${keyword}: ${key}`);
143
+ }
144
+ for (const keyword of ["items", "prefixItems", "anyOf", "oneOf", "allOf"]) {
145
+ const part = schema[keyword];
146
+ if (part === undefined) continue;
147
+ if (Array.isArray(part))
148
+ part.forEach((item, index) => {
149
+ child(
150
+ item,
151
+ `${keyword} [${index + 1}]`,
152
+ undefined,
153
+ ["allOf", "anyOf", "oneOf"].includes(keyword) ? requirements : undefined,
154
+ );
155
+ });
156
+ else child(part, keyword === "items" ? "items []" : keyword);
157
+ }
158
+ for (const keyword of ["additionalProperties", "not", "if", "then", "else"]) {
159
+ if (schema[keyword] !== undefined)
160
+ child(schema[keyword], keyword, undefined, ["if", "then", "else"].includes(keyword) ? requirements : undefined);
161
+ }
162
+ return node;
163
+ }
164
+ return parse(value, "Input");
165
+ }
166
+
167
+ function documentedReturn(codemode: string): string | undefined {
168
+ const result = /\btools\.[\w$]+\(args\)`? resolves to[ \t]+([^\n]+)(?:\n|$)/.exec(codemode);
169
+ let type = result?.[1]?.trim().replace(/\.$/, "");
170
+ if (type?.startsWith("`")) {
171
+ if (!type.endsWith("`") || type.length < 2) return undefined;
172
+ type = type.slice(1, -1);
173
+ } else type = type?.replace(/`$/, "");
174
+ if (!type || /[|&,:]$/.test(type)) return undefined;
175
+ // A wrapped/incomplete type is still readable in Codemode documentation, but
176
+ // must not be presented as a complete return declaration.
177
+ for (const [open, close] of [
178
+ ["{", "}"],
179
+ ["[", "]"],
180
+ ["(", ")"],
181
+ ] as const) {
182
+ if (type.split(open).length !== type.split(close).length) return undefined;
183
+ }
184
+ return type;
185
+ }
186
+
187
+ export interface ToolDoc {
188
+ id: number;
189
+ name: string;
190
+ description: string;
191
+ codemode: string;
192
+ returns?: Json;
193
+ parameters?: Json;
194
+ examples: { id: string; value: Json }[];
195
+ raw: Json;
196
+ }
197
+ export function toolDocuments(value: Json | undefined): ToolDoc[] {
198
+ if (!Array.isArray(value)) return [];
199
+ return value.map((raw, id) => {
200
+ const tool = record(raw);
201
+ const description = typeof tool?.description === "string" ? tool.description : "";
202
+ const marker = /(?:^|\n)[ \t]*Codemode:[ \t]*/i.exec(description);
203
+ const codemode = marker ? description.slice(marker.index + marker[0].length).trim() : "";
204
+ return {
205
+ id,
206
+ name: typeof tool?.name === "string" && tool.name ? tool.name : `Unnamed tool ${id + 1}`,
207
+ description: marker ? description.slice(0, marker.index).trimEnd() : description,
208
+ codemode,
209
+ returns: tool?.outputSchema ?? tool?.returnType ?? documentedReturn(codemode),
210
+ parameters: tool?.parameters ?? tool?.inputSchema,
211
+ examples: Array.isArray(tool?.examples)
212
+ ? tool.examples.map((value, index) => ({ id: `example-${index}`, value }))
213
+ : [],
214
+ raw,
215
+ };
216
+ });
217
+ }
package/src/web/trace.css CHANGED
@@ -148,6 +148,7 @@
148
148
  flex-shrink: 0;
149
149
  }
150
150
  .row-title > span {
151
+ max-width: 100%;
151
152
  color: var(--secondary);
152
153
  text-overflow: ellipsis;
153
154
  overflow: hidden;