@tenyokjs/devdevian-cli 0.3.1-alpha.1 → 0.4.1-alpha.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,96 @@
1
+ import { clean } from "./client.mjs";
2
+ import { wrap } from "./tui-model.mjs";
3
+ import { colors as c } from "./theme.mjs";
4
+
5
+ // Small, predictable terminal renderer. HTML and terminal hyperlinks are never executed.
6
+ export function syntaxSpans(text) {
7
+ const pattern =
8
+ /("(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`[^`]*`|\/\/.*$|#.*$|\b(?:async|await|function|return|const|let|var|if|else|throw|new|class|import|from|export|def|None|True|False|true|false|null|try|catch|for|while)\b|\b\d+(?:\.\d+)?\b)/g;
9
+ const parts = [];
10
+ let start = 0;
11
+ for (const match of text.matchAll(pattern)) {
12
+ if (match.index > start)
13
+ parts.push({ text: text.slice(start, match.index) });
14
+ const word = match[0];
15
+ parts.push({
16
+ text: word,
17
+ color: /^(\/\/|#)/.test(word)
18
+ ? c.muted
19
+ : /^["'`]/.test(word)
20
+ ? c.green
21
+ : /^\d/.test(word)
22
+ ? c.blue
23
+ : c.purple,
24
+ });
25
+ start = match.index + word.length;
26
+ }
27
+ if (start < text.length) parts.push({ text: text.slice(start) });
28
+ return parts;
29
+ }
30
+ export function markdownRows(value, width) {
31
+ let fence = null,
32
+ number = 0;
33
+ const rows = [];
34
+ for (const raw of clean(value).replace(/\t/g, " ").split("\n")) {
35
+ const marker = raw.match(/^\s*(`{3,}|~{3,})(.*)$/);
36
+ if (
37
+ marker &&
38
+ (!fence ||
39
+ (marker[1][0] === fence[0] &&
40
+ marker[1].length >= fence.length &&
41
+ !marker[2].trim()))
42
+ ) {
43
+ if (fence) {
44
+ fence = null;
45
+ rows.push({ text: "", color: c.muted });
46
+ } else {
47
+ fence = marker[1];
48
+ number = 0;
49
+ rows.push({
50
+ text:
51
+ "CODE SNIPPET" + (marker[2].trim() ? " · " + marker[2].trim() : ""),
52
+ color: c.blue,
53
+ bold: true,
54
+ });
55
+ }
56
+ continue;
57
+ }
58
+ if (fence) {
59
+ const prefix = String(++number).padStart(4) + " ";
60
+ for (const [i, line] of wrap(raw, Math.max(1, width - 6)).entries()) {
61
+ const text = (i ? " " : prefix) + line;
62
+ rows.push({ text, spans: syntaxSpans(text) });
63
+ }
64
+ continue;
65
+ }
66
+ const heading = /^#{1,6}\s+/.test(raw),
67
+ quote = /^>\s?/.test(raw);
68
+ const text = raw
69
+ .replace(/^#{1,6}\s+/, "")
70
+ .replace(/^>\s?/, "│ ")
71
+ .replace(/^\s*[-*+]\s+/, "• ")
72
+ .replace(/\[([^\]\n]+)\]\(([^)\n]+)\)/g, "$1 <$2>");
73
+ for (const line of wrap(text, width)) {
74
+ const parts = [];
75
+ let start = 0;
76
+ for (const match of line.matchAll(/\*\*([^*]+)\*\*|`([^`]+)`/g)) {
77
+ if (match.index > start)
78
+ parts.push({ text: line.slice(start, match.index) });
79
+ parts.push({
80
+ text: match[1] || match[2],
81
+ bold: !!match[1],
82
+ color: match[2] ? c.blue : undefined,
83
+ });
84
+ start = match.index + match[0].length;
85
+ }
86
+ parts.push({ text: line.slice(start) });
87
+ rows.push({
88
+ text: parts.map((p) => p.text).join(""),
89
+ spans: parts,
90
+ bold: heading,
91
+ color: heading ? c.purple : quote ? c.muted : c.text,
92
+ });
93
+ }
94
+ }
95
+ return rows;
96
+ }
package/lib/theme.mjs ADDED
@@ -0,0 +1,13 @@
1
+ // One palette for the workspace, file picker and review panes.
2
+ export const colors = Object.freeze({
3
+ bg: "#0b1020",
4
+ panel: "#11182b",
5
+ line: "#303d60",
6
+ muted: "#8e9ab8",
7
+ text: "#e5eaff",
8
+ blue: "#86baff",
9
+ purple: "#b5a0ff",
10
+ green: "#7bdcbd",
11
+ red: "#ffa0b4",
12
+ selected: "#29365b",
13
+ });
package/lib/tui-data.mjs CHANGED
@@ -1,3 +1,14 @@
1
+ import { createHash } from "node:crypto";
2
+ export function assertSnippet(code, hash) {
3
+ if (
4
+ createHash("sha256")
5
+ .update(code || "")
6
+ .digest("hex") !== hash
7
+ )
8
+ throw Error(
9
+ "Code changed while loading. Press Shift+R to refresh before reviewing.",
10
+ );
11
+ }
1
12
  export const navigation = [
2
13
  ["inbox", "Inbox", "Replies & mentions"],
3
14
  ["feed", "Public feed", "Developer commons"],
@@ -10,6 +21,39 @@ export const navigation = [
10
21
  ["devices", "Connected terminals", "Manage CLI access"],
11
22
  ["help", "Help", "Keys & commands"],
12
23
  ];
24
+ export async function notificationRoute(api, item) {
25
+ if (item.code_review_id)
26
+ return {
27
+ view: "feedback",
28
+ postId: item.post_id,
29
+ id: item.code_review_id,
30
+ page: 1,
31
+ targetReply: item.feedback_reply_id || null,
32
+ };
33
+ if (!item.reply_id)
34
+ return { view: "post", id: item.post_id, tab: "overview", page: 1 };
35
+ const post = await api(`/posts/${item.post_id}?reply_id=${item.reply_id}`);
36
+ const target = post.reply_path?.at(-1);
37
+ if (!target || post.reply_target_missing)
38
+ throw Object.assign(Error("The linked reply is no longer available."), {
39
+ status: 404,
40
+ });
41
+ return target.parent_id
42
+ ? {
43
+ view: "thread",
44
+ postId: item.post_id,
45
+ parent: { id: target.parent_id },
46
+ page: target.page,
47
+ targetReply: item.reply_id,
48
+ }
49
+ : {
50
+ view: "post",
51
+ id: item.post_id,
52
+ tab: "replies",
53
+ page: target.page,
54
+ targetReply: item.reply_id,
55
+ };
56
+ }
13
57
  export async function loadPage(api, route, signal) {
14
58
  const get = (path) => api(path, { signal }),
15
59
  page = route.page || 1;
@@ -74,18 +118,32 @@ export async function loadPage(api, route, signal) {
74
118
  };
75
119
  }
76
120
  if (route.view === "post") {
77
- const post = await get("/posts/" + route.id + "?reply_page=" + page);
121
+ const post = await get(
122
+ "/posts/" +
123
+ route.id +
124
+ (route.targetReply
125
+ ? "?reply_id=" + route.targetReply
126
+ : "?reply_page=" + page),
127
+ );
78
128
  let items = post.replies,
79
129
  pagination = post.reply_pagination;
130
+ let snippet_hash;
80
131
  if (route.tab === "feedback") {
81
132
  const feedback = await get(
82
- "/posts/" + route.id + "/code-reviews?page=" + page,
133
+ "/posts/" +
134
+ route.id +
135
+ "/code-reviews?page=" +
136
+ page +
137
+ (route.status ? "&status=" + route.status : ""),
83
138
  );
139
+ assertSnippet(post.code, feedback.snippet_hash);
140
+ snippet_hash = feedback.snippet_hash;
84
141
  items = feedback.items;
85
142
  pagination = feedback.pagination;
86
143
  }
87
144
  return {
88
145
  post,
146
+ snippet_hash,
89
147
  items,
90
148
  pagination,
91
149
  title: post.title,
@@ -128,6 +186,7 @@ export async function loadPage(api, route, signal) {
128
186
  get("/posts/" + route.postId),
129
187
  get(`/posts/${route.postId}/code-reviews?target_id=${route.id}`),
130
188
  ]);
189
+ assertSnippet(post.code, notes.snippet_hash);
131
190
  const note = notes.items.find((item) => item.id === Number(route.id));
132
191
  if (!note) throw Error("This feedback is no longer available.");
133
192
  const replies = await get(
@@ -136,6 +195,7 @@ export async function loadPage(api, route, signal) {
136
195
  return {
137
196
  post,
138
197
  note,
198
+ snippet_hash: notes.snippet_hash,
139
199
  items: [
140
200
  { ...note, entryType: "note" },
141
201
  ...replies.items.map((item) => ({
@@ -163,11 +223,18 @@ export function contentLines(route, data) {
163
223
  "Page Up / Down Scroll a conversation or list",
164
224
  "N / A New conversation / ask a question",
165
225
  "R Reply in a conversation; refresh a list",
226
+ "V / W Open review workspace / switch code and discussion",
227
+ "Space / C Select a line range / comment on selected code",
228
+ "F Filter remarks: all, open, resolved",
229
+ "X / ? Context actions (edit, delete, solution, live updates)",
230
+ "Live updates 45s active / 3min idle; pause in X actions or --no-live",
166
231
  "C Add feedback on code lines",
167
232
  "S Resolve / reopen selected code feedback",
168
233
  "E / D Edit / delete your own feedback or reply",
169
234
  "O Open this conversation in your browser",
170
235
  "/ Focus the command / search bar",
236
+ "Ctrl+E Edit a multiline field in VISUAL / EDITOR",
237
+ "Ctrl+L Save a draft and reconnect an expired session",
171
238
  "Ctrl+S Preview the current form",
172
239
  "Ctrl+O / Ctrl+G Select a code file / Git diff in a review form",
173
240
  "/drafts Restore locally saved drafts for this account/project",