indusagi-coding-agent 0.2.12 → 0.2.14
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/.env.review.example +25 -0
- package/README.md +30 -0
- package/REVIEW_AGENT.md +137 -0
- package/dist/entry.js +1861 -979
- package/dist/guardrails.js +212 -36
- package/dist/index.js +2829 -1057
- package/dist/review-agent.js +4461 -0
- package/dist/review-server.js +4633 -0
- package/package.json +24 -7
- package/src/boot/auth-vault.ts +4 -5
- package/src/boot/contract.ts +1 -1
- package/src/boot/runners/addon-wiring.ts +111 -3
- package/src/boot/runners/benchmark-integrity.ts +107 -0
- package/src/boot/runners/repl-runner.ts +2 -1
- package/src/boot/runners/session.ts +26 -12
- package/src/boot/server-token.ts +20 -11
- package/src/boot/stages.ts +3 -6
- package/src/briefing/context-docs.ts +144 -12
- package/src/channels/oneshot.ts +6 -0
- package/src/conductor/catalog/catalog.ts +3 -1
- package/src/conductor/conductor.ts +77 -17
- package/src/conductor/contract.ts +9 -7
- package/src/conductor/index.ts +1 -1
- package/src/conductor/transcript-store/store.ts +4 -8
- package/src/console/auth-status.ts +9 -4
- package/src/console/components/AgentsView.ts +2 -2
- package/src/console/components/BackgroundAgents.ts +15 -2
- package/src/console/components/Banner.ts +20 -9
- package/src/console/components/Composer.ts +57 -82
- package/src/console/components/ScrollViewport.ts +46 -0
- package/src/console/components/TerminalConsole.ts +165 -162
- package/src/console/components/WorkingIndicator.ts +6 -5
- package/src/console/components/banner-sweep.ts +5 -1
- package/src/console/components/welcome.ts +3 -3
- package/src/console/contract.ts +10 -5
- package/src/console/index.ts +7 -7
- package/src/console/input/complete.ts +2 -2
- package/src/console/input/dir-reader.ts +3 -1
- package/src/console/input/graphemes.ts +22 -0
- package/src/console/input/keymap.ts +6 -4
- package/src/console/mount.ts +45 -3
- package/src/console/overlays/auth.ts +28 -8
- package/src/console/overlays/boards.ts +13 -40
- package/src/console/overlays/host.ts +10 -0
- package/src/console/overlays/pickers.ts +35 -30
- package/src/console/reducer.ts +12 -5
- package/src/console/slash/builtins.ts +4 -3
- package/src/console/slash/commands/integrations.ts +13 -14
- package/src/console/slash/commands/transcript.ts +3 -8
- package/src/console/slash/commands/workbench.ts +4 -4
- package/src/console/test-support.ts +48 -0
- package/src/index.ts +1 -0
- package/src/integrations/sarvam/config.ts +15 -13
- package/src/kit/external-editor.ts +17 -1
- package/src/kit/run-shell.ts +31 -0
- package/src/launch/catalog.ts +4 -2
- package/src/launch/contract.ts +1 -0
- package/src/launch/invocation/flags.ts +13 -1
- package/src/launch/invocation/read.ts +4 -3
- package/src/launch/login.ts +23 -10
- package/src/launch/oauth.ts +3 -0
- package/src/models.ts +166 -0
- package/src/review-agent/config.ts +75 -0
- package/src/review-agent/contract.ts +59 -0
- package/src/review-agent/github.ts +242 -0
- package/src/review-agent/index.ts +8 -0
- package/src/review-agent/prompt.ts +132 -0
- package/src/review-agent/reviewer.ts +118 -0
- package/src/review-agent/server.ts +221 -0
- package/src/review-agent/service.ts +165 -0
- package/src/review-agent/supabase.ts +209 -0
- package/src/review-agent/webhook.ts +76 -0
- package/src/security/private-state.ts +122 -0
- package/src/window-budget/summarize/condense.ts +4 -1
- package/src/workspace/brand.ts +1 -1
- package/src/workspace/locator.ts +2 -2
- package/supabase/migrations/20260827031803_indusagi_review_agent.sql +109 -0
- package/supabase/migrations/20260827035747_atomic_webhook_enqueue.sql +75 -0
- package/supabase/migrations/20260827040104_check_run_reconciliation.sql +8 -0
- package/supabase/migrations/20260827041049_owned_job_leases.sql +80 -0
- package/supabase/migrations/20260827042500_static_token_webhooks.sql +75 -0
|
@@ -9,7 +9,9 @@ import { basename } from "node:path";
|
|
|
9
9
|
import type { InkThemeAdapter } from "indusagi/react-ink";
|
|
10
10
|
import type { StartupChangelog, StartupMap, StartupNotice } from "../startup.js";
|
|
11
11
|
import { MASCOTS, WHATS_NEW, pickWelcomeVariant, mascotRows, type MascotFace } from "./welcome.js";
|
|
12
|
-
import { welcomeLine } from "./banner-sweep.js";
|
|
12
|
+
import { welcomeLine, rowGradient } from "./banner-sweep.js";
|
|
13
|
+
import chalk from "chalk";
|
|
14
|
+
import { useTerminalSize } from "./ScrollViewport.js";
|
|
13
15
|
|
|
14
16
|
const BRAND = "IndusCode";
|
|
15
17
|
const COMPACT_EMBLEM = "▛▜▙▟";
|
|
@@ -23,6 +25,11 @@ const WORDMARK_ROWS: readonly string[] = [
|
|
|
23
25
|
"╚═╝╚═╝ ╚═══╝╚═════╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝"
|
|
24
26
|
];
|
|
25
27
|
|
|
28
|
+
function StartupSummary({theme, startup}: {theme: InkThemeAdapter; startup?: StartupMap}) {
|
|
29
|
+
if (!startup?.sections.length) return null;
|
|
30
|
+
return jsx(Text, {wrap:"truncate-end", children:theme.muted(startup.sections.map(section => `${section.title}: ${section.count}`).join(" · "))});
|
|
31
|
+
}
|
|
32
|
+
|
|
26
33
|
function StartupNotices({ theme, notices }: { theme: InkThemeAdapter; notices: readonly StartupNotice[] }) {
|
|
27
34
|
if (notices.length === 0) return null;
|
|
28
35
|
return jsx(Box, {
|
|
@@ -98,10 +105,10 @@ function ShellLine({ theme, workspace, branch }: { theme: InkThemeAdapter; works
|
|
|
98
105
|
});
|
|
99
106
|
}
|
|
100
107
|
|
|
101
|
-
function Wordmark({ theme }: { theme: InkThemeAdapter }) {
|
|
108
|
+
function Wordmark({ theme, sweep }: { theme: InkThemeAdapter; sweep?: boolean }) {
|
|
102
109
|
return jsx(Box, {
|
|
103
110
|
flexDirection: "column",
|
|
104
|
-
children: WORDMARK_ROWS.map((line, index) => jsx(Text, { children: theme.color("accent", line) }, index))
|
|
111
|
+
children: WORDMARK_ROWS.map((line, index) => jsx(Text, { children: sweep ? chalk.hex(rowGradient(index, WORDMARK_ROWS.length, theme.colors.accent, theme.colors.customMessage))(line) : theme.color("accent", line) }, index))
|
|
105
112
|
});
|
|
106
113
|
}
|
|
107
114
|
|
|
@@ -127,7 +134,7 @@ function paintMascot(theme: InkThemeAdapter, tone: string, text: string) {
|
|
|
127
134
|
}
|
|
128
135
|
}
|
|
129
136
|
|
|
130
|
-
function Mascot({ theme, face }: { theme: InkThemeAdapter; face: MascotFace }) {
|
|
137
|
+
function Mascot({ theme, face, sweep }: { theme: InkThemeAdapter; face: MascotFace; sweep?: boolean }) {
|
|
131
138
|
return jsx(Box, {
|
|
132
139
|
flexDirection: "column",
|
|
133
140
|
alignItems: "center",
|
|
@@ -137,7 +144,7 @@ function Mascot({ theme, face }: { theme: InkThemeAdapter; face: MascotFace }) {
|
|
|
137
144
|
Box,
|
|
138
145
|
{
|
|
139
146
|
children: row.map((seg, segIndex) =>
|
|
140
|
-
jsx(Text, { children: paintMascot(theme, seg.tone, seg.text) }, segIndex)
|
|
147
|
+
jsx(Text, { children: sweep && seg.tone === "head" ? chalk.hex(rowGradient(rowIndex, 5, theme.colors.accent, theme.colors.customMessage))(seg.text) : paintMascot(theme, seg.tone, seg.text) }, segIndex)
|
|
141
148
|
)
|
|
142
149
|
},
|
|
143
150
|
rowIndex
|
|
@@ -174,6 +181,7 @@ export interface BannerProps {
|
|
|
174
181
|
}
|
|
175
182
|
|
|
176
183
|
export function Banner(props: BannerProps) {
|
|
184
|
+
const { columns } = useTerminalSize();
|
|
177
185
|
const {
|
|
178
186
|
theme,
|
|
179
187
|
modelId,
|
|
@@ -187,15 +195,16 @@ export function Banner(props: BannerProps) {
|
|
|
187
195
|
variantSeed,
|
|
188
196
|
whatsNew,
|
|
189
197
|
notices,
|
|
190
|
-
changelog
|
|
198
|
+
changelog, sweep, startup
|
|
191
199
|
} = props;
|
|
192
200
|
const noticeList = notices ?? [];
|
|
193
|
-
if (quiet || compact) {
|
|
201
|
+
if (quiet || compact || columns < 76) {
|
|
194
202
|
return jsxs(Box, {
|
|
195
203
|
flexDirection: "column",
|
|
196
204
|
marginBottom: 1,
|
|
197
205
|
children: [
|
|
198
206
|
jsx(StartupNotices, { theme, notices: noticeList }),
|
|
207
|
+
jsx(StartupSummary, {theme, startup}),
|
|
199
208
|
jsxs(Box, {
|
|
200
209
|
children: [
|
|
201
210
|
compact
|
|
@@ -225,8 +234,9 @@ export function Banner(props: BannerProps) {
|
|
|
225
234
|
marginBottom: 1,
|
|
226
235
|
children: [
|
|
227
236
|
jsx(StartupNotices, { theme, notices: noticeList }),
|
|
237
|
+
jsx(StartupSummary, {theme, startup}),
|
|
228
238
|
jsx(ShellLine, { theme, workspace, branch }),
|
|
229
|
-
jsx(Box, { marginTop: 1, children: jsx(Wordmark, { theme }) }),
|
|
239
|
+
jsx(Box, { marginTop: 1, children: jsx(Wordmark, { theme, sweep }) }),
|
|
230
240
|
jsxs(Box, {
|
|
231
241
|
marginTop: 1,
|
|
232
242
|
flexDirection: "column",
|
|
@@ -266,6 +276,7 @@ export function Banner(props: BannerProps) {
|
|
|
266
276
|
marginBottom: 1,
|
|
267
277
|
children: [
|
|
268
278
|
jsx(StartupNotices, { theme, notices: noticeList }),
|
|
279
|
+
jsx(StartupSummary, {theme, startup}),
|
|
269
280
|
jsxs(Box, {
|
|
270
281
|
flexDirection: "column",
|
|
271
282
|
borderStyle: "round",
|
|
@@ -292,7 +303,7 @@ export function Banner(props: BannerProps) {
|
|
|
292
303
|
marginRight: 2,
|
|
293
304
|
children: [
|
|
294
305
|
jsx(Text, { bold: true, children: theme.color("text", greeting) }),
|
|
295
|
-
jsx(Mascot, { theme, face: variant.mascot }),
|
|
306
|
+
jsx(Mascot, { theme, face: variant.mascot, sweep }),
|
|
296
307
|
jsx(Text, { children: theme.dim(metaLine) }),
|
|
297
308
|
jsx(Text, { children: theme.dim(shortenWorkspace(workspace)) })
|
|
298
309
|
]
|
|
@@ -1,29 +1,31 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
7
|
-
import
|
|
1
|
+
/** Bounded, grapheme-aware editor and selection-following completion menu. */
|
|
2
|
+
import { Box, Text } from 'indusagi/react-host/ink';
|
|
3
|
+
import { jsx, jsxs } from 'indusagi/react-host/jsx-runtime';
|
|
4
|
+
import stringWidth from 'string-width';
|
|
5
|
+
import type { InkThemeAdapter } from 'indusagi/react-ink';
|
|
6
|
+
import type { Suggestion } from '../input/index.js';
|
|
7
|
+
import { snapGrapheme } from '../input/graphemes.js';
|
|
8
|
+
import { useTerminalSize } from './ScrollViewport.js';
|
|
8
9
|
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
10
|
+
const segmenter = new Intl.Segmenter(undefined, {granularity:'grapheme'});
|
|
11
|
+
interface Cell { text: string; caret: boolean }
|
|
12
|
+
function editorLines(buffer: string, caret: number, width: number) {
|
|
13
|
+
const lines: Cell[][] = [[]];
|
|
14
|
+
let column = 0;
|
|
15
|
+
caret = snapGrapheme(buffer, caret);
|
|
16
|
+
for (const {segment, index} of segmenter.segment(buffer + ' ')) {
|
|
17
|
+
const cursor = index === caret;
|
|
18
|
+
if (segment === '\n') {
|
|
19
|
+
if (cursor) lines.at(-1)!.push({text:' ', caret:true});
|
|
20
|
+
lines.push([]); column=0; continue;
|
|
21
|
+
}
|
|
22
|
+
const text = segment === '\t' ? ' ' : segment.replace(/[\x00-\x1f\x7f]/g,'');
|
|
23
|
+
const cells = stringWidth(text);
|
|
24
|
+
if (column + cells > width && column > 0) {lines.push([]);column=0;}
|
|
25
|
+
lines.at(-1)!.push({text:cells > width ? '?' : text,caret:cursor});
|
|
26
|
+
column += Math.min(width,cells);
|
|
27
|
+
}
|
|
28
|
+
return lines;
|
|
27
29
|
}
|
|
28
30
|
|
|
29
31
|
export interface ComposerProps {
|
|
@@ -33,62 +35,35 @@ export interface ComposerProps {
|
|
|
33
35
|
readonly busy: boolean;
|
|
34
36
|
readonly suggestions: readonly Suggestion[];
|
|
35
37
|
readonly highlight: number;
|
|
38
|
+
readonly paddingX?: number;
|
|
36
39
|
}
|
|
37
40
|
|
|
38
|
-
export function Composer({
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}),
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const active = index === highlight;
|
|
68
|
-
const name = clampName(suggestion.label, nameWidth);
|
|
69
|
-
return jsxs(
|
|
70
|
-
Box,
|
|
71
|
-
{
|
|
72
|
-
children: [
|
|
73
|
-
jsx(Box, {
|
|
74
|
-
width: 2,
|
|
75
|
-
flexShrink: 0,
|
|
76
|
-
children: jsx(Text, { children: active ? theme.color("accent", "▸") : " " })
|
|
77
|
-
}),
|
|
78
|
-
jsx(Box, {
|
|
79
|
-
width: nameWidth,
|
|
80
|
-
flexShrink: 0,
|
|
81
|
-
children: jsx(Text, { bold: active, children: theme.color("customMessage", name) })
|
|
82
|
-
}),
|
|
83
|
-
jsx(Text, { children: " " }),
|
|
84
|
-
jsx(Text, { children: theme.muted(suggestion.detail) })
|
|
85
|
-
]
|
|
86
|
-
},
|
|
87
|
-
suggestion.value
|
|
88
|
-
);
|
|
89
|
-
})
|
|
90
|
-
})
|
|
91
|
-
: null
|
|
92
|
-
]
|
|
93
|
-
});
|
|
94
|
-
}
|
|
41
|
+
export function Composer({theme,buffer,caret,suggestions,highlight,paddingX=1}: ComposerProps) {
|
|
42
|
+
const {columns,rows} = useTerminalSize();
|
|
43
|
+
const padding = Math.max(0, Math.min(3, Math.floor(paddingX), Math.floor((columns-8)/2)));
|
|
44
|
+
const sendWidth = columns >= 32 ? 7 : 0;
|
|
45
|
+
const width = Math.max(1,columns - 2 - padding*2 - 2 - sendWidth);
|
|
46
|
+
const allLines = editorLines(buffer,caret,width);
|
|
47
|
+
const caretRow = Math.max(0,allLines.findIndex(line=>line.some(cell=>cell.caret)));
|
|
48
|
+
const editorBudget = Math.max(1,Math.min(6,Math.floor(rows/4)));
|
|
49
|
+
const top = Math.max(0,Math.min(caretRow-editorBudget+1,allLines.length-editorBudget));
|
|
50
|
+
const lines = allLines.slice(top,top+editorBudget);
|
|
51
|
+
const budget = Math.max(1,Math.min(8,rows-lines.length-12));
|
|
52
|
+
const start = Math.max(0,Math.min(highlight-budget+1,suggestions.length-budget));
|
|
53
|
+
const shown = suggestions.slice(start,start+budget);
|
|
54
|
+
const nameWidth = Math.max(1,Math.min(24,Math.floor(columns/3)));
|
|
55
|
+
return jsxs(Box,{flexDirection:'column',flexShrink:0,children:[
|
|
56
|
+
jsx(Box,{borderStyle:'round',borderColor:theme.colors.borderMuted??'gray',paddingX:padding,flexDirection:'column',children:
|
|
57
|
+
lines.map((line,index)=>jsxs(Box,{height:1,flexShrink:0,children:[
|
|
58
|
+
jsx(Box,{width:2,flexShrink:0,children:jsx(Text,{children:theme.color('customMessage',index+top===0?'› ':'· ')})}),
|
|
59
|
+
jsx(Box,{width,flexShrink:0,children:jsx(Text,{wrap:'truncate-end',children:line.map((cell,i)=>jsx(Text,{inverse:cell.caret,children:cell.text},i))})}),
|
|
60
|
+
sendWidth ? jsx(Box,{width:sendWidth,flexShrink:0,children:jsx(Text,{children:index===0?theme.dim(' ↵ send'):''})}) : null
|
|
61
|
+
]},index))
|
|
62
|
+
}),
|
|
63
|
+
shown.length ? jsx(Box,{flexDirection:'column',marginTop:1,children:shown.map((suggestion,index)=>jsxs(Box,{height:1,children:[
|
|
64
|
+
jsx(Box,{width:2,flexShrink:0,children:jsx(Text,{children:start+index===highlight?theme.color('accent','▸'):' '})}),
|
|
65
|
+
jsx(Box,{width:nameWidth,flexShrink:0,children:jsx(Text,{wrap:'truncate-end',bold:start+index===highlight,children:theme.color('customMessage',suggestion.label)})}),
|
|
66
|
+
jsx(Box,{width:Math.max(1,columns-nameWidth-2),children:jsx(Text,{wrap:'truncate-end',children:theme.muted(' '+suggestion.detail)})})
|
|
67
|
+
]},suggestion.value))}) : null
|
|
68
|
+
]});
|
|
69
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import React, { useEffect, useState } from 'indusagi/react-host';
|
|
2
|
+
import { Box, useInput } from 'indusagi/react-host/ink';
|
|
3
|
+
import { measureElement, useStdout, type DOMElement } from 'ink';
|
|
4
|
+
import { jsx } from 'indusagi/react-host/jsx-runtime';
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
export function useTerminalSize() {
|
|
8
|
+
const { stdout } = useStdout();
|
|
9
|
+
const read = () => ({ columns: Math.max(1, stdout.columns || 80), rows: Math.max(1, stdout.rows || 24) });
|
|
10
|
+
const [size, setSize] = useState(read);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const resize = () => setSize(read());
|
|
13
|
+
stdout.on('resize', resize);
|
|
14
|
+
return () => { stdout.off('resize', resize); };
|
|
15
|
+
}, [stdout]);
|
|
16
|
+
return size;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Row-bounded Ink viewport. Page keys scroll; new output follows only at the bottom. */
|
|
20
|
+
export function ScrollViewport({ children, active = true, follow = true }: { children: ReactNode; active?: boolean; follow?: boolean }) {
|
|
21
|
+
const outer = React.useRef<DOMElement>(null);
|
|
22
|
+
const inner = React.useRef<DOMElement>(null);
|
|
23
|
+
const [metrics, setMetrics] = useState({ height: 1, content: 1 });
|
|
24
|
+
const [offset, setOffset] = useState(0);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!outer.current || !inner.current) return;
|
|
27
|
+
const height = measureElement(outer.current).height;
|
|
28
|
+
const content = measureElement(inner.current).height;
|
|
29
|
+
if (metrics.height !== height || metrics.content !== content) {
|
|
30
|
+
const oldMax = Math.max(0, metrics.content - metrics.height);
|
|
31
|
+
const newMax = Math.max(0, content - height);
|
|
32
|
+
if (follow) setOffset(n => n === 0 ? 0 : Math.max(0, Math.min(newMax, n + newMax - oldMax)));
|
|
33
|
+
setMetrics({height, content});
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
const max = Math.max(0, metrics.content - metrics.height);
|
|
37
|
+
const scroll = Math.min(max, offset);
|
|
38
|
+
useInput((_input, key) => {
|
|
39
|
+
const step = Math.max(1, metrics.height - 2);
|
|
40
|
+
if (key.pageUp) setOffset(n => Math.min(max, n + (follow ? step : -step)));
|
|
41
|
+
if (key.pageDown) setOffset(n => Math.max(0, Math.min(max, n + (follow ? -step : step))));
|
|
42
|
+
}, {isActive: active});
|
|
43
|
+
const top = follow ? Math.max(0, max - scroll) : Math.max(0, scroll);
|
|
44
|
+
return jsx(Box, {ref: outer, flexDirection:'column', flexGrow:1, flexBasis:0, minHeight:1, overflow:'hidden',
|
|
45
|
+
children:jsx(Box, {ref:inner, flexDirection:'column', flexShrink:0, marginTop:-top, children})});
|
|
46
|
+
}
|