@rmrdeveloper/sideroom-pi 8.11.0 → 8.13.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/README.md +49 -5
- package/assets/artifacts/GUIDELINES_TEMPLATE.md +3 -3
- package/assets/visual-context/browser/INSTALL.md +69 -0
- package/assets/visual-context/browser/background.js +578 -0
- package/assets/visual-context/browser/content.js +198 -0
- package/assets/visual-context/browser/icons/README.md +9 -0
- package/assets/visual-context/browser/icons/icon-128.png +0 -0
- package/assets/visual-context/browser/icons/icon-16.png +0 -0
- package/assets/visual-context/browser/icons/icon-32.png +0 -0
- package/assets/visual-context/browser/icons/icon-48.png +0 -0
- package/assets/visual-context/browser/manifest.json +20 -0
- package/assets/visual-context/browser/popup.css +19 -0
- package/assets/visual-context/browser/popup.html +27 -0
- package/assets/visual-context/browser/popup.js +141 -0
- package/extensions/done/guard.ts +42 -37
- package/extensions/done/index.ts +3 -2
- package/extensions/done/model.ts +1 -5
- package/extensions/explain/guard.ts +13 -11
- package/extensions/explain/model.ts +2 -25
- package/extensions/guidelines/review.ts +31 -13
- package/extensions/jev/client.ts +22 -1
- package/extensions/jev/context.ts +36 -3
- package/extensions/jev/guard.ts +113 -34
- package/extensions/jev/model.ts +90 -12
- package/extensions/jev/workflow.ts +188 -0
- package/extensions/persona/execute.ts +4 -6
- package/extensions/persona/index.ts +3 -2
- package/extensions/persona/model.ts +1 -31
- package/extensions/persona/prompt.ts +3 -1
- package/extensions/shared/settle.ts +63 -13
- package/extensions/visual-context/bridge.ts +240 -0
- package/extensions/visual-context/broker.ts +292 -0
- package/extensions/visual-context/envelope.ts +34 -0
- package/extensions/visual-context/index.ts +328 -0
- package/extensions/visual-context/literal.ts +34 -0
- package/extensions/visual-context/markup.ts +104 -0
- package/extensions/visual-context/match.ts +222 -0
- package/extensions/visual-context/model.ts +180 -0
- package/extensions/visual-context/pairing.ts +114 -0
- package/extensions/visual-context/parse-astro.ts +166 -0
- package/extensions/visual-context/parse-html.ts +72 -0
- package/extensions/visual-context/parse-jsx.ts +157 -0
- package/extensions/visual-context/parse-svelte.ts +128 -0
- package/extensions/visual-context/parse-vue.ts +133 -0
- package/extensions/visual-context/session.ts +192 -0
- package/extensions/visual-context/source.ts +197 -0
- package/extensions/visual-context/ui.ts +57 -0
- package/package.json +16 -7
- package/skills/sideroom-architecture/SKILL.md +5 -3
- package/skills/sideroom-codebase-design/LICENSE.txt +21 -0
- package/skills/sideroom-codebase-design/SKILL.md +151 -0
- package/skills/sideroom-codebase-design/references/growth.md +62 -0
- package/skills/sideroom-domain-modeling/SKILL.md +1 -1
- package/skills/sideroom-domain-scaffold/SKILL.md +6 -4
- package/skills/sideroom-grill/SKILL.md +6 -4
- package/skills/sideroom-guidelines/SKILL.md +9 -7
- package/skills/sideroom-guidelines/references/languages/go.md +3 -3
- package/skills/sideroom-guidelines/references/languages/java.md +3 -3
- package/skills/sideroom-guidelines/references/languages/javascript.md +25 -3
- package/skills/sideroom-guidelines/references/languages/php-laravel.md +3 -3
- package/skills/sideroom-guidelines/references/languages/python.md +3 -3
- package/skills/sideroom-guidelines/references/languages/rust.md +3 -3
- package/skills/sideroom-guidelines/references/languages/typescript.md +27 -3
- package/skills/sideroom-persona/SKILL.md +15 -4
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from '@earendil-works/pi-coding-agent';
|
|
5
|
+
import { type Static, Type } from 'typebox';
|
|
6
|
+
import { Check } from 'typebox/value';
|
|
7
|
+
import {
|
|
8
|
+
addSelection,
|
|
9
|
+
assertSelectionCapacity,
|
|
10
|
+
MAX_CANDIDATES,
|
|
11
|
+
MAX_SELECTIONS,
|
|
12
|
+
type PendingSelection,
|
|
13
|
+
parseSelection,
|
|
14
|
+
SelectionSchema,
|
|
15
|
+
selectionContext,
|
|
16
|
+
VISUAL_SNAPSHOT,
|
|
17
|
+
type VisualSelection,
|
|
18
|
+
} from './model.ts';
|
|
19
|
+
import { candidateIsCurrent, resolveSelection } from './source.ts';
|
|
20
|
+
|
|
21
|
+
const CandidateSchema = Type.Object(
|
|
22
|
+
{
|
|
23
|
+
path: Type.String({
|
|
24
|
+
minLength: 1,
|
|
25
|
+
maxLength: 2_048,
|
|
26
|
+
pattern: '^[^\\x00-\\x1f\\x7f]*$',
|
|
27
|
+
}),
|
|
28
|
+
startLine: Type.Integer({ minimum: 1 }),
|
|
29
|
+
endLine: Type.Integer({ minimum: 1 }),
|
|
30
|
+
fingerprint: Type.String({ pattern: '^[a-f0-9]{64}$' }),
|
|
31
|
+
score: Type.Integer({ minimum: 0 }),
|
|
32
|
+
reasons: Type.Array(Type.String({ maxLength: 200 }), { maxItems: 40 }),
|
|
33
|
+
},
|
|
34
|
+
{ additionalProperties: false },
|
|
35
|
+
);
|
|
36
|
+
const SnapshotSchema = Type.Object(
|
|
37
|
+
{
|
|
38
|
+
pending: Type.Array(
|
|
39
|
+
Type.Object(
|
|
40
|
+
{
|
|
41
|
+
selection: SelectionSchema,
|
|
42
|
+
resolution: Type.Object(
|
|
43
|
+
{
|
|
44
|
+
candidates: Type.Array(CandidateSchema, {
|
|
45
|
+
maxItems: MAX_CANDIDATES,
|
|
46
|
+
}),
|
|
47
|
+
truncated: Type.Boolean(),
|
|
48
|
+
capped: Type.Boolean(),
|
|
49
|
+
},
|
|
50
|
+
{ additionalProperties: false },
|
|
51
|
+
),
|
|
52
|
+
chosen: Type.Optional(CandidateSchema),
|
|
53
|
+
},
|
|
54
|
+
{ additionalProperties: false },
|
|
55
|
+
),
|
|
56
|
+
{ maxItems: MAX_SELECTIONS },
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
{ additionalProperties: false },
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
export interface VisualStore {
|
|
63
|
+
pending: readonly PendingSelection[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createVisualStore(): VisualStore {
|
|
67
|
+
return { pending: [] };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function commitPending({
|
|
71
|
+
store,
|
|
72
|
+
pi,
|
|
73
|
+
pending,
|
|
74
|
+
}: {
|
|
75
|
+
readonly store: VisualStore;
|
|
76
|
+
readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
|
|
77
|
+
readonly pending: readonly PendingSelection[];
|
|
78
|
+
}): void {
|
|
79
|
+
store.pending = pending;
|
|
80
|
+
pi.appendEntry(VISUAL_SNAPSHOT, { pending });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function receiveSelection({
|
|
84
|
+
store,
|
|
85
|
+
pi,
|
|
86
|
+
cwd,
|
|
87
|
+
selection,
|
|
88
|
+
}: {
|
|
89
|
+
readonly store: VisualStore;
|
|
90
|
+
readonly pi: Pick<ExtensionAPI, 'appendEntry'>;
|
|
91
|
+
readonly cwd: string;
|
|
92
|
+
readonly selection: VisualSelection;
|
|
93
|
+
}): { readonly candidates: number } {
|
|
94
|
+
const existing = store.pending.find(
|
|
95
|
+
(entry) => entry.selection.id === selection.id,
|
|
96
|
+
);
|
|
97
|
+
if (existing) {
|
|
98
|
+
return { candidates: existing.resolution.candidates.length };
|
|
99
|
+
}
|
|
100
|
+
assertSelectionCapacity(store.pending);
|
|
101
|
+
const resolution = resolveSelection(cwd, selection);
|
|
102
|
+
commitPending({
|
|
103
|
+
store,
|
|
104
|
+
pi,
|
|
105
|
+
pending: addSelection(store.pending, { selection, resolution }),
|
|
106
|
+
});
|
|
107
|
+
return { candidates: resolution.candidates.length };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function restorePending({
|
|
111
|
+
store,
|
|
112
|
+
ctx,
|
|
113
|
+
}: {
|
|
114
|
+
readonly store: VisualStore;
|
|
115
|
+
readonly ctx: Pick<ExtensionContext, 'sessionManager' | 'cwd'>;
|
|
116
|
+
}): void {
|
|
117
|
+
store.pending = [];
|
|
118
|
+
const branch = ctx.sessionManager.getBranch();
|
|
119
|
+
for (let index = branch.length - 1; index >= 0; index -= 1) {
|
|
120
|
+
const entry = branch[index];
|
|
121
|
+
if (entry?.type !== 'custom' || entry.customType !== VISUAL_SNAPSHOT) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (!Check(SnapshotSchema, entry.data)) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
const snapshot: Static<typeof SnapshotSchema> = entry.data;
|
|
128
|
+
store.pending = snapshot.pending.flatMap((pending) => {
|
|
129
|
+
const candidates = pending.resolution.candidates.filter(
|
|
130
|
+
(candidate) =>
|
|
131
|
+
candidate.startLine <= candidate.endLine &&
|
|
132
|
+
candidateIsCurrent(ctx.cwd, candidate),
|
|
133
|
+
);
|
|
134
|
+
let selection: VisualSelection;
|
|
135
|
+
try {
|
|
136
|
+
selection = parseSelection(pending.selection);
|
|
137
|
+
} catch (cause) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
'Cannot restore browser context: invalid page address in the session.',
|
|
140
|
+
{ cause },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
const chosen = candidates.find(
|
|
144
|
+
(candidate) =>
|
|
145
|
+
candidate.path === pending.chosen?.path &&
|
|
146
|
+
candidate.startLine === pending.chosen.startLine &&
|
|
147
|
+
candidate.fingerprint === pending.chosen.fingerprint,
|
|
148
|
+
);
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
selection,
|
|
152
|
+
resolution: { ...pending.resolution, candidates },
|
|
153
|
+
chosen,
|
|
154
|
+
},
|
|
155
|
+
];
|
|
156
|
+
});
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function prepareContext({
|
|
162
|
+
store,
|
|
163
|
+
cwd,
|
|
164
|
+
}: {
|
|
165
|
+
readonly store: VisualStore;
|
|
166
|
+
readonly cwd: string;
|
|
167
|
+
}):
|
|
168
|
+
| { readonly text: string; readonly selectionIds: readonly string[] }
|
|
169
|
+
| undefined {
|
|
170
|
+
if (store.pending.length === 0) {
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
const pending = store.pending.map((entry) => {
|
|
174
|
+
const candidates = entry.resolution.candidates.filter((candidate) =>
|
|
175
|
+
candidateIsCurrent(cwd, candidate),
|
|
176
|
+
);
|
|
177
|
+
const chosen =
|
|
178
|
+
entry.chosen && candidates.includes(entry.chosen)
|
|
179
|
+
? entry.chosen
|
|
180
|
+
: undefined;
|
|
181
|
+
return {
|
|
182
|
+
...entry,
|
|
183
|
+
resolution: { ...entry.resolution, candidates },
|
|
184
|
+
chosen,
|
|
185
|
+
};
|
|
186
|
+
});
|
|
187
|
+
const context = selectionContext(pending);
|
|
188
|
+
return {
|
|
189
|
+
text: context,
|
|
190
|
+
selectionIds: pending.map((entry) => entry.selection.id),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from 'node:fs';
|
|
2
|
+
import { extname, join, relative, sep } from 'node:path';
|
|
3
|
+
import ignore from 'ignore';
|
|
4
|
+
import { projectRelativePath } from '../shared/file-path.ts';
|
|
5
|
+
import { isMissingFile } from '../shared/read-file.ts';
|
|
6
|
+
import { InvalidMarkup } from './markup.ts';
|
|
7
|
+
import { matchSource, sourceFingerprint } from './match.ts';
|
|
8
|
+
import {
|
|
9
|
+
MAX_CANDIDATES,
|
|
10
|
+
type SourceCandidate,
|
|
11
|
+
type SourceResolution,
|
|
12
|
+
type VisualSelection,
|
|
13
|
+
} from './model.ts';
|
|
14
|
+
|
|
15
|
+
const SOURCE_EXTENSIONS = new Set([
|
|
16
|
+
'.html',
|
|
17
|
+
'.htm',
|
|
18
|
+
'.vue',
|
|
19
|
+
'.svelte',
|
|
20
|
+
'.astro',
|
|
21
|
+
'.jsx',
|
|
22
|
+
'.tsx',
|
|
23
|
+
]);
|
|
24
|
+
const SKIP_DIRECTORIES = new Set([
|
|
25
|
+
'node_modules',
|
|
26
|
+
'dist',
|
|
27
|
+
'build',
|
|
28
|
+
'coverage',
|
|
29
|
+
'vendor',
|
|
30
|
+
]);
|
|
31
|
+
const IGNORE_FILES = ['.gitignore', '.ignore', '.fdignore'];
|
|
32
|
+
const MAX_ENTRIES = 4_096;
|
|
33
|
+
const MAX_FILE_BYTES = 262_144;
|
|
34
|
+
const MAX_SOURCE_BYTES = 8_388_608;
|
|
35
|
+
const MAX_DEPTH = 16;
|
|
36
|
+
const SENSITIVE_NAME =
|
|
37
|
+
/secret|credential|password|private.?key|(?:^|[._-])env(?:[._-]|$)/iu;
|
|
38
|
+
|
|
39
|
+
export function resolveSelection(
|
|
40
|
+
cwd: string,
|
|
41
|
+
selection: VisualSelection,
|
|
42
|
+
): SourceResolution {
|
|
43
|
+
const root = realpathSync(cwd);
|
|
44
|
+
const candidates: SourceCandidate[] = [];
|
|
45
|
+
let entriesRead = 0;
|
|
46
|
+
let sourceBytes = 0;
|
|
47
|
+
let truncated = false;
|
|
48
|
+
function walk({
|
|
49
|
+
directory,
|
|
50
|
+
patterns,
|
|
51
|
+
depth,
|
|
52
|
+
}: {
|
|
53
|
+
readonly directory: string;
|
|
54
|
+
readonly patterns: readonly string[];
|
|
55
|
+
readonly depth: number;
|
|
56
|
+
}): void {
|
|
57
|
+
if (depth > MAX_DEPTH) {
|
|
58
|
+
truncated = true;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const localPatterns = [...patterns, ...ignorePatterns(directory, root)];
|
|
62
|
+
const matcher = ignore().add(localPatterns);
|
|
63
|
+
for (const entry of readdirSync(directory, { withFileTypes: true }).sort(
|
|
64
|
+
(left, right) => left.name.localeCompare(right.name),
|
|
65
|
+
)) {
|
|
66
|
+
entriesRead += 1;
|
|
67
|
+
if (entriesRead > MAX_ENTRIES || sourceBytes >= MAX_SOURCE_BYTES) {
|
|
68
|
+
truncated = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (
|
|
72
|
+
entry.name.startsWith('.') ||
|
|
73
|
+
[...entry.name].some(
|
|
74
|
+
(character) =>
|
|
75
|
+
character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127,
|
|
76
|
+
) ||
|
|
77
|
+
SENSITIVE_NAME.test(entry.name) ||
|
|
78
|
+
entry.isSymbolicLink()
|
|
79
|
+
) {
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
const absolutePath = join(directory, entry.name);
|
|
83
|
+
const path = relative(root, absolutePath).split(sep).join('/');
|
|
84
|
+
if (matcher.ignores(entry.isDirectory() ? `${path}/` : path)) {
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (entry.isDirectory()) {
|
|
88
|
+
if (!SKIP_DIRECTORIES.has(entry.name)) {
|
|
89
|
+
walk({
|
|
90
|
+
directory: absolutePath,
|
|
91
|
+
patterns: localPatterns,
|
|
92
|
+
depth: depth + 1,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
if (
|
|
98
|
+
!entry.isFile() ||
|
|
99
|
+
!SOURCE_EXTENSIONS.has(extname(entry.name).toLowerCase())
|
|
100
|
+
) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const size = statSync(absolutePath).size;
|
|
104
|
+
if (size > MAX_FILE_BYTES || sourceBytes + size > MAX_SOURCE_BYTES) {
|
|
105
|
+
truncated = true;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
sourceBytes += size;
|
|
109
|
+
const source = readFileSync(absolutePath, 'utf8');
|
|
110
|
+
try {
|
|
111
|
+
candidates.push(...matchSource({ path, source, selection }));
|
|
112
|
+
} catch (cause) {
|
|
113
|
+
if (cause instanceof InvalidMarkup) {
|
|
114
|
+
truncated = true;
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`Cannot inspect visual source ${path}`, { cause });
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
walk({ directory: root, patterns: [], depth: 0 });
|
|
122
|
+
candidates.sort(
|
|
123
|
+
(left, right) =>
|
|
124
|
+
right.score - left.score ||
|
|
125
|
+
left.path.localeCompare(right.path) ||
|
|
126
|
+
left.startLine - right.startLine,
|
|
127
|
+
);
|
|
128
|
+
return {
|
|
129
|
+
candidates: candidates.slice(0, MAX_CANDIDATES),
|
|
130
|
+
truncated,
|
|
131
|
+
capped: candidates.length > MAX_CANDIDATES,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function ignorePatterns(directory: string, root: string): readonly string[] {
|
|
136
|
+
const prefix = relative(root, directory).split(sep).join('/');
|
|
137
|
+
const patterns: string[] = [];
|
|
138
|
+
for (const file of IGNORE_FILES) {
|
|
139
|
+
let source: string;
|
|
140
|
+
try {
|
|
141
|
+
source = readFileSync(join(directory, file), 'utf8');
|
|
142
|
+
} catch (cause) {
|
|
143
|
+
if (isMissingFile(cause)) {
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
throw new Error(`Cannot read ignore file ${file}`, { cause });
|
|
147
|
+
}
|
|
148
|
+
for (const line of source.split(/\r?\n/u)) {
|
|
149
|
+
if (!line || line.startsWith('#')) {
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const negated = line.startsWith('!');
|
|
153
|
+
const pattern = negated ? line.slice(1) : line;
|
|
154
|
+
const nested = prefix
|
|
155
|
+
? `${prefix}/${pattern.startsWith('/') ? pattern.slice(1) : pattern.includes('/') ? pattern : `**/${pattern}`}`
|
|
156
|
+
: pattern;
|
|
157
|
+
patterns.push(`${negated ? '!' : ''}${nested}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return patterns;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function candidateIsCurrent(
|
|
164
|
+
cwd: string,
|
|
165
|
+
candidate: SourceCandidate,
|
|
166
|
+
): boolean {
|
|
167
|
+
try {
|
|
168
|
+
const absolutePath = realpathSync(join(cwd, candidate.path));
|
|
169
|
+
if (
|
|
170
|
+
projectRelativePath(cwd, absolutePath) === undefined ||
|
|
171
|
+
statSync(absolutePath).size > MAX_FILE_BYTES
|
|
172
|
+
) {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
const source = readFileSync(absolutePath, 'utf8');
|
|
176
|
+
return (
|
|
177
|
+
sourceFingerprint(source) === candidate.fingerprint &&
|
|
178
|
+
candidate.endLine <= source.split('\n').length
|
|
179
|
+
);
|
|
180
|
+
} catch (cause) {
|
|
181
|
+
if (isGone(cause)) {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
throw new Error(`Cannot verify selected source ${candidate.path}`, {
|
|
185
|
+
cause,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// A path that turned into a file on the way reports ENOTDIR, which means the
|
|
191
|
+
// remembered source is gone rather than unreadable.
|
|
192
|
+
function isGone(cause: unknown): boolean {
|
|
193
|
+
return (
|
|
194
|
+
isMissingFile(cause) ||
|
|
195
|
+
(cause instanceof Error && 'code' in cause && cause.code === 'ENOTDIR')
|
|
196
|
+
);
|
|
197
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
import type { ExtensionContext, Theme } from '@earendil-works/pi-coding-agent';
|
|
3
|
+
import {
|
|
4
|
+
candidateLabel,
|
|
5
|
+
elementLabel,
|
|
6
|
+
type PendingSelection,
|
|
7
|
+
VISUAL_WIDGET,
|
|
8
|
+
} from './model.ts';
|
|
9
|
+
|
|
10
|
+
export const BROWSER_EXTENSION_DIRECTORY = fileURLToPath(
|
|
11
|
+
new URL('../../assets/visual-context/browser/', import.meta.url),
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
function pendingLabel({ resolution, chosen }: PendingSelection): string {
|
|
15
|
+
if (chosen) {
|
|
16
|
+
return `${candidateLabel(chosen)} (selected candidate)`;
|
|
17
|
+
}
|
|
18
|
+
const truncated = resolution.truncated ? ' · search truncated' : '';
|
|
19
|
+
const capped = resolution.capped ? ' · more candidates' : '';
|
|
20
|
+
return `${String(resolution.candidates.length)} candidates${truncated}${capped}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function renderVisualWidget(
|
|
24
|
+
pending: readonly PendingSelection[],
|
|
25
|
+
theme: Theme,
|
|
26
|
+
): readonly string[] {
|
|
27
|
+
return [
|
|
28
|
+
'',
|
|
29
|
+
theme.fg(
|
|
30
|
+
'dim',
|
|
31
|
+
`Browser elements (${String(pending.length)}) · F7: review`,
|
|
32
|
+
),
|
|
33
|
+
...pending.map((entry) =>
|
|
34
|
+
theme.fg(
|
|
35
|
+
entry.chosen ? 'accent' : 'muted',
|
|
36
|
+
`${elementLabel(entry.selection)} — ${pendingLabel(entry)}`,
|
|
37
|
+
),
|
|
38
|
+
),
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function refreshVisualWidget(
|
|
43
|
+
pending: readonly PendingSelection[],
|
|
44
|
+
ctx: Pick<ExtensionContext, 'ui' | 'mode'>,
|
|
45
|
+
): void {
|
|
46
|
+
if (ctx.mode !== 'tui') {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (pending.length === 0) {
|
|
50
|
+
ctx.ui.setWidget(VISUAL_WIDGET, undefined);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
ctx.ui.setWidget(VISUAL_WIDGET, (_tui, theme) => ({
|
|
54
|
+
render: () => [...renderVisualWidget(pending, theme)],
|
|
55
|
+
invalidate() {},
|
|
56
|
+
}));
|
|
57
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rmrdeveloper/sideroom-pi",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.13.0",
|
|
4
4
|
"description": "Pi coding-agent package that asks clarifying questions, shows a live todo board, enforces coding guidelines, and will not call it done while your checks are red.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,14 +17,15 @@
|
|
|
17
17
|
"commitlint": "commitlint",
|
|
18
18
|
"types": "tsc --noEmit",
|
|
19
19
|
"test": "node --test 'extensions/**/*.test.ts' 'scripts/**/*.test.mjs'",
|
|
20
|
-
"lint": "biome lint extensions scripts commitlint.config.mjs",
|
|
21
|
-
"lint:fix": "biome lint --write extensions scripts commitlint.config.mjs",
|
|
22
|
-
"format": "biome format --write extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
|
|
23
|
-
"format:check": "biome format extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
|
|
20
|
+
"lint": "biome lint extensions assets/visual-context/browser scripts commitlint.config.mjs",
|
|
21
|
+
"lint:fix": "biome lint --write extensions assets/visual-context/browser scripts commitlint.config.mjs",
|
|
22
|
+
"format": "biome format --write extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
|
|
23
|
+
"format:check": "biome format extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json",
|
|
24
24
|
"preview": "node scripts/render-preview.mjs",
|
|
25
25
|
"cache:report": "node scripts/cache-report.mjs",
|
|
26
|
+
"package:browser": "node scripts/package-browser.mjs",
|
|
26
27
|
"check:if-blocks": "node scripts/check-if-blocks.mjs",
|
|
27
|
-
"check": "npm run types && npm run check:if-blocks && biome check extensions scripts package.json biome.json commitlint.config.mjs .changeset/config.json && npm test"
|
|
28
|
+
"check": "npm run types && npm run check:if-blocks && biome check extensions assets/visual-context/browser scripts package.json biome.json commitlint.config.mjs .changeset/config.json && npm test"
|
|
28
29
|
},
|
|
29
30
|
"engines": {
|
|
30
31
|
"node": ">=22.19.0"
|
|
@@ -92,10 +93,18 @@
|
|
|
92
93
|
"@earendil-works/pi-coding-agent": "0.87.1",
|
|
93
94
|
"@earendil-works/pi-tui": "0.87.1",
|
|
94
95
|
"@types/node": "^22.20.1",
|
|
96
|
+
"@types/ws": "^8.18.2",
|
|
97
|
+
"fflate": "0.8.3",
|
|
95
98
|
"typebox": "1.3.7",
|
|
96
99
|
"typescript": "^5.6.0"
|
|
97
100
|
},
|
|
98
101
|
"dependencies": {
|
|
99
|
-
"
|
|
102
|
+
"@astrojs/compiler": "^4.0.0",
|
|
103
|
+
"@babel/parser": "^7.29.9",
|
|
104
|
+
"@vue/compiler-dom": "^3.5.43",
|
|
105
|
+
"ignore": "7.0.5",
|
|
106
|
+
"parse5": "^8.0.1",
|
|
107
|
+
"svelte": "^5.57.2",
|
|
108
|
+
"ws": "^8.22.0"
|
|
100
109
|
}
|
|
101
110
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: sideroom-architecture
|
|
3
|
-
description: "
|
|
3
|
+
description: "Resolve architectural choices before code: module boundaries, data ownership, API or integration contracts, persistence, failure and retry behavior, deployment, capacity, cost, or provider choice. Use when designing or changing these and the repository or user has not settled the choice, even if the feature requirements are clear. Skip routine changes that reuse established contracts; use sideroom-grill when the broader plan is also open."
|
|
4
4
|
license: MIT
|
|
5
5
|
metadata:
|
|
6
6
|
author: RMRdeveloper
|
|
@@ -25,8 +25,10 @@ throughput, availability), operations, cost, lock-in, or the difficulty of
|
|
|
25
25
|
changing course later. It runs under `sideroom-grill` for the architectural
|
|
26
26
|
part of an interview, and on its own when only the architecture is open.
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
Clear feature requirements do not settle the architecture by themselves.
|
|
29
|
+
Load this skill if a consequential choice remains, even when it is reversible.
|
|
30
|
+
Do not load it for routine changes that reuse established boundaries and
|
|
31
|
+
contracts, or for a word conflict — that is `sideroom-domain-modeling`.
|
|
30
32
|
|
|
31
33
|
## Hard Rules
|
|
32
34
|
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Matt Pocock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|