@huseynsnmz/pi-suite 0.1.0 → 0.2.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +2 -1
  3. package/package.json +6 -7
  4. package/packages/board/package.json +0 -2
  5. package/packages/board/src/core/rows.mjs +21 -5
  6. package/packages/board/src/ui/dashboard.ts +36 -20
  7. package/packages/commands/LICENSE +21 -0
  8. package/packages/commands/README.md +19 -0
  9. package/packages/commands/package.json +22 -0
  10. package/packages/commands/src/index.ts +43 -0
  11. package/packages/commands/src/security-review.ts +109 -0
  12. package/packages/frame/README.md +1 -1
  13. package/packages/frame/package.json +1 -4
  14. package/packages/frame/src/data/github.ts +1 -1
  15. package/packages/frame/src/data/mode.ts +28 -2
  16. package/packages/frame/src/data/snapshot.ts +1 -1
  17. package/packages/frame/src/data/start-notes.ts +1 -1
  18. package/packages/frame/src/data/totals.ts +2 -2
  19. package/packages/frame/src/data/visits.ts +1 -1
  20. package/packages/frame/src/index.ts +22 -2
  21. package/packages/frame/src/kit/LICENSE +21 -0
  22. package/packages/frame/src/kit/app/attribution.ts +30 -0
  23. package/packages/frame/src/kit/app/builder.ts +186 -0
  24. package/packages/frame/src/kit/app/claim.ts +27 -0
  25. package/packages/frame/src/kit/app/feature.ts +91 -0
  26. package/packages/frame/src/kit/app/host.ts +214 -0
  27. package/packages/frame/src/kit/app/renderers.ts +62 -0
  28. package/packages/frame/src/kit/app/scope.ts +125 -0
  29. package/packages/frame/src/kit/board/model.ts +337 -0
  30. package/packages/frame/src/kit/contract.ts +79 -0
  31. package/packages/frame/src/kit/contracts/accounts.ts +33 -0
  32. package/packages/frame/src/kit/contracts/ask.ts +120 -0
  33. package/packages/frame/src/kit/contracts/board.ts +6 -0
  34. package/packages/frame/src/kit/contracts/by.ts +22 -0
  35. package/packages/frame/src/kit/contracts/calls.ts +46 -0
  36. package/packages/frame/src/kit/contracts/events.ts +89 -0
  37. package/packages/frame/src/kit/contracts/mode.ts +26 -0
  38. package/packages/frame/src/kit/contracts/names.ts +12 -0
  39. package/packages/frame/src/kit/contracts/peers.ts +77 -0
  40. package/packages/frame/src/kit/contracts/prompts.ts +8 -0
  41. package/packages/frame/src/kit/contracts/screen.ts +75 -0
  42. package/packages/frame/src/kit/contracts/skills.ts +81 -0
  43. package/packages/frame/src/kit/contracts/sources.ts +81 -0
  44. package/packages/frame/src/kit/contracts/views.ts +42 -0
  45. package/packages/frame/src/kit/control.ts +59 -0
  46. package/packages/frame/src/kit/dark-theme.ts +41 -0
  47. package/packages/frame/src/kit/decode.ts +210 -0
  48. package/packages/frame/src/kit/events/log.ts +313 -0
  49. package/packages/frame/src/kit/format.ts +25 -0
  50. package/packages/frame/src/kit/host.ts +4 -0
  51. package/packages/frame/src/kit/index.ts +25 -0
  52. package/packages/frame/src/kit/legacy-tasks.ts +39 -0
  53. package/packages/frame/src/kit/overlay-images.ts +36 -0
  54. package/packages/frame/src/kit/paths.ts +16 -0
  55. package/packages/frame/src/kit/screen/client.ts +74 -0
  56. package/packages/frame/src/kit/screen/feature.ts +64 -0
  57. package/packages/frame/src/kit/screen/model.ts +182 -0
  58. package/packages/frame/src/kit/screen/view.ts +838 -0
  59. package/packages/frame/src/kit/settings/files.ts +90 -0
  60. package/packages/frame/src/kit/settings/setting.ts +57 -0
  61. package/packages/frame/src/kit/settings/store.ts +192 -0
  62. package/packages/frame/src/kit/sources/file.ts +129 -0
  63. package/packages/frame/src/kit/sources/follow.ts +155 -0
  64. package/packages/frame/src/kit/testing.ts +260 -0
  65. package/packages/frame/src/kit/totals.ts +72 -0
  66. package/packages/frame/src/kit/ui/entry.ts +75 -0
  67. package/packages/frame/src/kit/ui/frame.ts +212 -0
  68. package/packages/frame/src/kit/ui/glyphs.ts +132 -0
  69. package/packages/frame/src/kit/ui/scroll.ts +14 -0
  70. package/packages/frame/src/render/diff.ts +55 -2
  71. package/packages/frame/src/render/fit.ts +1 -1
  72. package/packages/frame/src/render/format.ts +1 -1
  73. package/packages/frame/src/render/segments.ts +1 -1
  74. package/packages/frame/src/render/streaming.ts +1 -1
  75. package/packages/frame/src/render/tools.ts +1 -1
  76. package/packages/frame/src/settings.ts +1 -1
  77. package/packages/frame/src/ui/editor.ts +1 -1
  78. package/packages/frame/src/ui/effort.ts +1 -1
  79. package/packages/frame/src/ui/header.ts +1 -1
  80. package/packages/frame/src/ui/outputs.ts +1 -1
  81. package/packages/frame/src/ui/pi-entries.ts +28 -6
  82. package/packages/frame/src/ui/previews.ts +1 -1
  83. package/packages/frame/src/ui/queue.ts +1 -1
  84. package/packages/frame/src/ui/todos.ts +1 -1
  85. package/packages/frame/src/ui/tool-frame.ts +1 -1
  86. package/packages/frame/src/view/state.ts +1 -1
  87. package/packages/frame/src/view/transcript.ts +1 -1
  88. package/packages/frame/src/view/view.ts +1 -1
  89. package/packages/web/package.json +1 -3
  90. package/packages/web/src/extract.ts +2 -2
  91. package/packages/web/src/markdown.ts +122 -0
  92. package/src/index.ts +1 -1
  93. package/src/settings.ts +1 -0
@@ -0,0 +1,74 @@
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import {
4
+ APPLY,
5
+ type ApplyRequest,
6
+ DONE,
7
+ LIST,
8
+ type ListRequest,
9
+ type RowView,
10
+ RUN,
11
+ type RunDone,
12
+ type RunRequest,
13
+ type TabView,
14
+ } from "../contracts/screen.ts";
15
+ import type { Json } from "../control.ts";
16
+ import { isObject } from "../decode.ts";
17
+
18
+ type Events = ExtensionAPI["events"];
19
+
20
+ /** Same-titled tabs from different packages merge. */
21
+ export function listTabs(events: Events): TabView[] {
22
+ const request: ListRequest = { tabs: [] };
23
+ events.emit(LIST, request);
24
+
25
+ const merged: TabView[] = [];
26
+ for (const tab of request.tabs) {
27
+ if (tab.rows.length === 0) continue;
28
+ const same = merged.find((entry) => entry.title === tab.title);
29
+ if (same === undefined) {
30
+ merged.push({ title: tab.title, sections: [...tab.sections], rows: [...tab.rows] });
31
+ continue;
32
+ }
33
+ for (const section of tab.sections)
34
+ if (!same.sections.includes(section)) same.sections.push(section);
35
+ same.rows.push(...tab.rows);
36
+ }
37
+ return merged;
38
+ }
39
+
40
+ export function applyRow(
41
+ events: Events,
42
+ row: RowView,
43
+ op: ApplyRequest["op"],
44
+ value?: Json,
45
+ ): string | undefined {
46
+ const request: ApplyRequest = { feature: row.feature, id: row.id, op };
47
+ if (value !== undefined) request.value = value;
48
+ events.emit(APPLY, request);
49
+ if (request.answer === undefined) return `${row.feature} is not running`;
50
+ return request.answer.error;
51
+ }
52
+
53
+ export function runRow(events: Events, row: RowView): Promise<Omit<RunDone, "request">> {
54
+ const id = crypto.randomUUID();
55
+ return new Promise((resolve) => {
56
+ const stop = events.on(DONE, (data) => {
57
+ if (!isObject(data) || data.request !== id) return;
58
+ stop();
59
+ resolve({
60
+ ...(typeof data.text === "string" ? { text: data.text } : {}),
61
+ ...(typeof data.error === "string" ? { error: data.error } : {}),
62
+ });
63
+ });
64
+
65
+ const request: RunRequest = { feature: row.feature, id: row.id, request: id };
66
+ events.emit(RUN, request);
67
+ const refused =
68
+ request.answer === undefined ? `${row.feature} is not running` : request.answer.error;
69
+ if (refused !== undefined) {
70
+ stop();
71
+ resolve({ error: refused });
72
+ }
73
+ });
74
+ }
@@ -0,0 +1,64 @@
1
+ // Every app brings this feature; the claim keeps the first copy.
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Key } from "@earendil-works/pi-tui";
4
+
5
+ import type { Feature, FeatureScope } from "../app/feature.ts";
6
+ import { formatValue } from "../control.ts";
7
+ import { hideImagesUnderOverlays } from "../overlay-images.ts";
8
+ import { applyRow, listTabs } from "./client.ts";
9
+ import { ScreenModel } from "./model.ts";
10
+ import { type ScreenResult, ScreenView } from "./view.ts";
11
+
12
+ export const settingsScreen: Feature = {
13
+ id: "settings-screen",
14
+ description: "One screen for every setting of the harness",
15
+ setup(scope) {
16
+ scope.registerCommand("harness", {
17
+ description: "Settings for every harness piece (also Alt+S)",
18
+ getArgumentCompletions: (prefix) => {
19
+ const typed = prefix.trim().toLowerCase();
20
+ const tabs = listTabs(scope.events).map((tab) => tab.title.toLowerCase());
21
+ const matches = tabs.filter((tab) => tab.startsWith(typed));
22
+ return matches.length === 0 ? null : matches.map((tab) => ({ value: tab, label: tab }));
23
+ },
24
+ handler: (args, ctx) => openScreen(scope, ctx, args),
25
+ });
26
+ scope.registerShortcut(Key.alt("s"), {
27
+ description: "Settings for every harness piece",
28
+ handler: (ctx) => openScreen(scope, ctx, ""),
29
+ });
30
+ },
31
+ };
32
+
33
+ async function openScreen(scope: FeatureScope, ctx: ExtensionContext, tab: string): Promise<void> {
34
+ if (ctx.mode !== "tui") {
35
+ ctx.ui.notify("the settings screen needs the interactive terminal", "warning");
36
+ return;
37
+ }
38
+ const model = new ScreenModel(listTabs(scope.events));
39
+ if (tab.trim() !== "") model.openTab(tab);
40
+
41
+ // Pi's own editor takes over for long text, so the screen closes and reopens where it was.
42
+ for (;;) {
43
+ // oxlint-disable-next-line no-await-in-loop -- one screen at a time
44
+ const result = await ctx.ui.custom<ScreenResult>(
45
+ (tui, theme, _keybindings, done) => {
46
+ hideImagesUnderOverlays(tui);
47
+ return new ScreenView({ tui, theme, events: scope.events, model, done });
48
+ },
49
+ { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", anchor: "top-left" } },
50
+ );
51
+ if (result?.kind !== "edit") return;
52
+
53
+ const row = result.row;
54
+ const current = typeof row.value === "string" ? row.value : "";
55
+ // oxlint-disable-next-line no-await-in-loop -- the screen waits for the editor
56
+ const text = await ctx.ui.editor(row.label, current);
57
+ if (text !== undefined && text !== current) {
58
+ const error = applyRow(scope.events, row, "set", text);
59
+ const shown = row.control === undefined ? "" : formatValue(row.control, text);
60
+ ctx.ui.notify(error ?? `${row.label}: ${shown}`, error === undefined ? "info" : "error");
61
+ }
62
+ model.refresh(listTabs(scope.events));
63
+ }
64
+ }
@@ -0,0 +1,182 @@
1
+ import { fuzzyFilter } from "@earendil-works/pi-tui";
2
+
3
+ import type { RowView, TabView } from "../contracts/screen.ts";
4
+ import { GLYPH } from "../ui/glyphs.ts";
5
+
6
+ export type Line = { kind: "heading"; title: string } | { kind: "row"; row: RowView; tab: string };
7
+
8
+ export interface SectionMark {
9
+ title: string;
10
+ active: boolean;
11
+ }
12
+
13
+ export class ScreenModel {
14
+ tabs: TabView[];
15
+ tab = 0;
16
+ query = "";
17
+ #cursor: string | undefined;
18
+ /** Built once per tab, query and list; the four mutators drop it. */
19
+ #lines: Line[] | undefined;
20
+
21
+ constructor(tabs: TabView[]) {
22
+ this.tabs = tabs;
23
+ this.#cursor = this.#firstKey();
24
+ }
25
+
26
+ get searching(): boolean {
27
+ return this.query !== "";
28
+ }
29
+
30
+ refresh(tabs: TabView[]): void {
31
+ const title = this.tabs[this.tab]?.title;
32
+ this.tabs = tabs;
33
+ this.#lines = undefined;
34
+ const index = tabs.findIndex((entry) => entry.title === title);
35
+ this.tab = index === -1 ? 0 : index;
36
+ if (this.#index() === -1) this.#cursor = this.#firstKey();
37
+ }
38
+
39
+ lines(): Line[] {
40
+ this.#lines ??= this.searching ? this.#results() : this.#tabLines();
41
+ return this.#lines;
42
+ }
43
+
44
+ selected(): Extract<Line, { kind: "row" }> | undefined {
45
+ const line = this.lines()[this.#index()];
46
+ return line?.kind === "row" ? line : undefined;
47
+ }
48
+
49
+ cursorLine(): number {
50
+ return this.#index();
51
+ }
52
+
53
+ move(delta: number): void {
54
+ const rows = this.#rowIndexes();
55
+ if (rows.length === 0) return;
56
+ const at = rows.indexOf(this.#index());
57
+ const next = Math.min(rows.length - 1, Math.max(0, (at === -1 ? 0 : at) + delta));
58
+ this.#select(rows[next]);
59
+ }
60
+
61
+ moveToEdge(edge: "first" | "last"): void {
62
+ const rows = this.#rowIndexes();
63
+ this.#select(edge === "first" ? rows[0] : rows.at(-1));
64
+ }
65
+
66
+ jumpSection(direction: 1 | -1): void {
67
+ const lines = this.lines();
68
+ const starts: number[] = [];
69
+ lines.forEach((line, index) => {
70
+ if (line.kind === "heading" && lines[index + 1]?.kind === "row") starts.push(index + 1);
71
+ });
72
+ if (starts.length === 0) return;
73
+
74
+ const at = this.#index();
75
+ const current = starts.findLastIndex((start) => start <= at);
76
+ const next = (current + direction + starts.length) % starts.length;
77
+ this.#select(starts[next]);
78
+ }
79
+
80
+ switchTab(delta: number): void {
81
+ if (this.searching || this.tabs.length === 0) return;
82
+ this.tab = (this.tab + delta + this.tabs.length) % this.tabs.length;
83
+ this.#lines = undefined;
84
+ this.#cursor = this.#firstKey();
85
+ }
86
+
87
+ openTab(title: string): boolean {
88
+ const wanted = title.trim().toLowerCase();
89
+ const index = this.tabs.findIndex((entry) => entry.title.toLowerCase().startsWith(wanted));
90
+ if (index === -1) return false;
91
+ this.tab = index;
92
+ this.#lines = undefined;
93
+ this.#cursor = this.#firstKey();
94
+ return true;
95
+ }
96
+
97
+ setQuery(query: string): void {
98
+ this.query = query;
99
+ this.#lines = undefined;
100
+ this.#cursor = this.#firstKey();
101
+ }
102
+
103
+ sections(): SectionMark[] {
104
+ const lines = this.lines();
105
+ const at = this.#index();
106
+ let active = "";
107
+ lines.forEach((line, index) => {
108
+ if (line.kind === "heading" && index <= at) active = line.title;
109
+ });
110
+ return lines
111
+ .filter((line): line is Extract<Line, { kind: "heading" }> => line.kind === "heading")
112
+ .map((line) => ({ title: line.title, active: line.title === active }));
113
+ }
114
+
115
+ #tabLines(): Line[] {
116
+ const tab = this.tabs[this.tab];
117
+ if (tab === undefined) return [];
118
+ const lines: Line[] = [];
119
+ for (const section of tab.sections) {
120
+ const rows = tab.rows.filter((row) => row.section === section);
121
+ if (rows.length === 0) continue;
122
+ lines.push({ kind: "heading", title: section });
123
+ for (const row of rows) lines.push({ kind: "row", row, tab: tab.title });
124
+ }
125
+ return lines;
126
+ }
127
+
128
+ #results(): Line[] {
129
+ const all = this.tabs.flatMap((tab) => tab.rows.map((row) => ({ row, tab: tab.title })));
130
+ // The query picks the rows; the order stays the tab's.
131
+ const matched = new Set(
132
+ fuzzyFilter(all, this.query, ({ row, tab }) =>
133
+ [row.label, row.section, tab, row.id].join(" "),
134
+ ),
135
+ );
136
+ const found = all.filter((hit) => matched.has(hit));
137
+
138
+ const groups = new Map<string, typeof found>();
139
+ for (const hit of found) {
140
+ const title = `${hit.tab} ${GLYPH.chevron} ${hit.row.section}`;
141
+ const group = groups.get(title) ?? [];
142
+ group.push(hit);
143
+ groups.set(title, group);
144
+ }
145
+ const lines: Line[] = [];
146
+ for (const [title, hits] of groups) {
147
+ lines.push({ kind: "heading", title });
148
+ for (const hit of hits) lines.push({ kind: "row", ...hit });
149
+ }
150
+ return lines;
151
+ }
152
+
153
+ #rowIndexes(): number[] {
154
+ const indexes: number[] = [];
155
+ this.lines().forEach((line, index) => {
156
+ if (line.kind === "row") indexes.push(index);
157
+ });
158
+ return indexes;
159
+ }
160
+
161
+ #index(): number {
162
+ if (this.#cursor === undefined) return -1;
163
+ return this.lines().findIndex(
164
+ (line) => line.kind === "row" && keyOf(line.row) === this.#cursor,
165
+ );
166
+ }
167
+
168
+ #select(index: number | undefined): void {
169
+ if (index === undefined) return;
170
+ const line = this.lines()[index];
171
+ if (line?.kind === "row") this.#cursor = keyOf(line.row);
172
+ }
173
+
174
+ #firstKey(): string | undefined {
175
+ const line = this.lines().find((entry) => entry.kind === "row");
176
+ return line?.kind === "row" ? keyOf(line.row) : undefined;
177
+ }
178
+ }
179
+
180
+ export function keyOf(row: RowView): string {
181
+ return `${row.feature}\u0000${row.id}`;
182
+ }