Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a88b6cf98d | ||
|
|
41e50322be | ||
|
|
f1db46ecbe | ||
|
|
c0904e6aaa | ||
|
|
1d9492aff5 | ||
|
|
9416fa6518 | ||
|
|
360e2f3af0 | ||
|
|
0119c7cb0e |
@@ -0,0 +1,85 @@
|
|||||||
|
package com.neatstudio.tmuxandroid;
|
||||||
|
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
final class ConversationMessage {
|
||||||
|
final String messageId;
|
||||||
|
final String sessionName;
|
||||||
|
final String role;
|
||||||
|
final String contentType;
|
||||||
|
final String content;
|
||||||
|
final String status;
|
||||||
|
final String toolName;
|
||||||
|
final String parentMessageId;
|
||||||
|
final String createdAt;
|
||||||
|
final boolean local;
|
||||||
|
|
||||||
|
ConversationMessage(
|
||||||
|
String messageId,
|
||||||
|
String sessionName,
|
||||||
|
String role,
|
||||||
|
String contentType,
|
||||||
|
String content,
|
||||||
|
String status,
|
||||||
|
String toolName,
|
||||||
|
String parentMessageId,
|
||||||
|
String createdAt,
|
||||||
|
boolean local
|
||||||
|
) {
|
||||||
|
this.messageId = messageId;
|
||||||
|
this.sessionName = sessionName;
|
||||||
|
this.role = role;
|
||||||
|
this.contentType = contentType;
|
||||||
|
this.content = content;
|
||||||
|
this.status = status;
|
||||||
|
this.toolName = toolName;
|
||||||
|
this.parentMessageId = parentMessageId;
|
||||||
|
this.createdAt = createdAt;
|
||||||
|
this.local = local;
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConversationMessage fromJson(JSONObject object) {
|
||||||
|
return new ConversationMessage(
|
||||||
|
value(object, "messageId", object.optString("id", "")),
|
||||||
|
object.optString("sessionName", ""),
|
||||||
|
object.optString("role", "assistant"),
|
||||||
|
object.optString("contentType", "text"),
|
||||||
|
object.optString("content", ""),
|
||||||
|
object.optString("status", "complete"),
|
||||||
|
object.optString("toolName", ""),
|
||||||
|
value(object, "parentMessageId", ""),
|
||||||
|
object.optString("createdAt", ""),
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static ConversationMessage localUser(String sessionName, String content) {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
return new ConversationMessage(
|
||||||
|
"local-" + now,
|
||||||
|
sessionName,
|
||||||
|
"user",
|
||||||
|
"text",
|
||||||
|
content,
|
||||||
|
"sending",
|
||||||
|
"",
|
||||||
|
"",
|
||||||
|
"~" + now,
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isTool() {
|
||||||
|
return "tool".equals(role)
|
||||||
|
|| "tool".equals(contentType)
|
||||||
|
|| "command".equals(contentType)
|
||||||
|
|| "code".equals(contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String value(JSONObject object, String key, String fallback) {
|
||||||
|
if (!object.has(key) || object.isNull(key)) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
return object.optString(key, fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -4,12 +4,17 @@ import android.graphics.Color;
|
|||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.text.SpannableStringBuilder;
|
import android.text.SpannableStringBuilder;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
import android.text.TextPaint;
|
||||||
import android.text.style.BackgroundColorSpan;
|
import android.text.style.BackgroundColorSpan;
|
||||||
|
import android.text.style.ClickableSpan;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
final class TerminalScreenBuffer {
|
final class TerminalScreenBuffer {
|
||||||
private static final int DEFAULT_FG = 0xffe6ebf2;
|
private static final int DEFAULT_FG = 0xffe6ebf2;
|
||||||
@@ -30,6 +35,10 @@ final class TerminalScreenBuffer {
|
|||||||
private boolean bold;
|
private boolean bold;
|
||||||
private boolean dim;
|
private boolean dim;
|
||||||
|
|
||||||
|
interface FocusToggle {
|
||||||
|
void toggle(String key);
|
||||||
|
}
|
||||||
|
|
||||||
TerminalScreenBuffer(int cols, int rows) {
|
TerminalScreenBuffer(int cols, int rows) {
|
||||||
resize(cols, rows);
|
resize(cols, rows);
|
||||||
}
|
}
|
||||||
@@ -121,42 +130,120 @@ final class TerminalScreenBuffer {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
CharSequence renderFocused() {
|
String renderTail(int maxRows) {
|
||||||
List<String> visible = new ArrayList<>();
|
List<String> visible = new ArrayList<>();
|
||||||
int hiddenRows = 0;
|
for (int row = rows - 1; row >= 0 && visible.size() < maxRows; row--) {
|
||||||
|
String text = rowText(row).trim();
|
||||||
|
if (!text.isEmpty()) {
|
||||||
|
visible.add(0, text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (visible.isEmpty()) {
|
||||||
|
return "Waiting for terminal output";
|
||||||
|
}
|
||||||
|
return String.join("\n", visible);
|
||||||
|
}
|
||||||
|
|
||||||
|
CharSequence renderFocused(Set<String> expandedBlocks, FocusToggle toggle) {
|
||||||
|
SpannableStringBuilder output = new SpannableStringBuilder();
|
||||||
|
List<String> hiddenRows = new ArrayList<>();
|
||||||
|
int hiddenStart = -1;
|
||||||
for (int row = 0; row < rows; row++) {
|
for (int row = 0; row < rows; row++) {
|
||||||
String text = rowText(row).trim();
|
String text = rowText(row).trim();
|
||||||
if (containsHan(text)) {
|
if (containsHan(text)) {
|
||||||
if (hiddenRows > 0) {
|
if (!hiddenRows.isEmpty()) {
|
||||||
visible.add("... 已折叠 " + hiddenRows + " 行终端内容 ...");
|
appendFocusBlock(output, hiddenRows, hiddenStart, row - 1, expandedBlocks, toggle);
|
||||||
hiddenRows = 0;
|
hiddenRows.clear();
|
||||||
|
hiddenStart = -1;
|
||||||
}
|
}
|
||||||
visible.add(text);
|
appendLine(output, text);
|
||||||
} else if (!text.isEmpty()) {
|
} else if (!text.isEmpty()) {
|
||||||
hiddenRows++;
|
if (hiddenStart < 0) {
|
||||||
|
hiddenStart = row;
|
||||||
|
}
|
||||||
|
hiddenRows.add(text);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (hiddenRows > 0) {
|
if (!hiddenRows.isEmpty()) {
|
||||||
visible.add("... 已折叠 " + hiddenRows + " 行终端内容 ...");
|
appendFocusBlock(output, hiddenRows, hiddenStart, rows - 1, expandedBlocks, toggle);
|
||||||
}
|
}
|
||||||
if (visible.isEmpty()) {
|
if (output.length() == 0) {
|
||||||
for (int row = Math.max(0, rows - 4); row < rows; row++) {
|
output.append("暂无可读内容");
|
||||||
String text = rowText(row).trim();
|
|
||||||
if (!text.isEmpty()) {
|
|
||||||
visible.add(text);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
StringBuilder output = new StringBuilder();
|
|
||||||
for (int index = 0; index < visible.size(); index++) {
|
|
||||||
if (index > 0) {
|
|
||||||
output.append('\n');
|
|
||||||
}
|
|
||||||
output.append(visible.get(index));
|
|
||||||
}
|
}
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void appendFocusBlock(
|
||||||
|
SpannableStringBuilder output,
|
||||||
|
List<String> lines,
|
||||||
|
int startRow,
|
||||||
|
int endRow,
|
||||||
|
Set<String> expandedBlocks,
|
||||||
|
FocusToggle toggle
|
||||||
|
) {
|
||||||
|
String key = startRow + ":" + endRow;
|
||||||
|
boolean expanded = expandedBlocks.contains(key);
|
||||||
|
String summary = focusSummary(lines);
|
||||||
|
int actionStart = output.length();
|
||||||
|
appendLine(output, (expanded ? "▼ " : "▶ ") + summary);
|
||||||
|
int actionEnd = output.length();
|
||||||
|
output.setSpan(new ClickableSpan() {
|
||||||
|
@Override
|
||||||
|
public void onClick(View widget) {
|
||||||
|
toggle.toggle(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDrawState(TextPaint paint) {
|
||||||
|
paint.setColor(0xff67da91);
|
||||||
|
paint.setUnderlineText(false);
|
||||||
|
paint.setFakeBoldText(true);
|
||||||
|
}
|
||||||
|
}, actionStart, actionEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
if (expanded) {
|
||||||
|
for (String line : lines) {
|
||||||
|
appendLine(output, " " + line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String focusSummary(List<String> lines) {
|
||||||
|
String joined = String.join(" ", lines).toLowerCase(Locale.ROOT);
|
||||||
|
String type;
|
||||||
|
if (joined.contains("error") || joined.contains("failed") || joined.contains("exception")) {
|
||||||
|
type = "错误输出";
|
||||||
|
} else if (joined.contains("working") || joined.contains("running")
|
||||||
|
|| joined.contains("waiting") || joined.contains("interrupt")) {
|
||||||
|
type = "运行状态";
|
||||||
|
} else if (joined.contains("test") || joined.contains("build") || joined.contains("compile")
|
||||||
|
|| joined.contains("gradle") || joined.contains("webpack") || joined.contains("npm")) {
|
||||||
|
type = "构建/测试";
|
||||||
|
} else if (joined.contains("git ") || joined.contains("commit") || joined.contains("push")) {
|
||||||
|
type = "Git 操作";
|
||||||
|
} else if (lines.get(0).startsWith(">") || lines.get(0).startsWith("$")
|
||||||
|
|| lines.get(0).startsWith("!")) {
|
||||||
|
type = "命令与输出";
|
||||||
|
} else {
|
||||||
|
type = "终端细节";
|
||||||
|
}
|
||||||
|
return type + " · " + lines.size() + " 行 · " + compactSummary(lines.get(0), 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String compactSummary(String text, int maxChars) {
|
||||||
|
String compact = text.replaceAll("\\s+", " ").trim();
|
||||||
|
if (compact.length() <= maxChars) {
|
||||||
|
return compact;
|
||||||
|
}
|
||||||
|
return compact.substring(0, maxChars - 1) + "…";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendLine(SpannableStringBuilder output, String text) {
|
||||||
|
if (output.length() > 0) {
|
||||||
|
output.append('\n');
|
||||||
|
}
|
||||||
|
output.append(text);
|
||||||
|
}
|
||||||
|
|
||||||
private int handleEscape(String text, int index) {
|
private int handleEscape(String text, int index) {
|
||||||
if (index + 1 >= text.length()) {
|
if (index + 1 >= text.length()) {
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
Reference in New Issue
Block a user